diff --git a/.deployment b/.deployment new file mode 100644 index 00000000..9d7d31aa --- /dev/null +++ b/.deployment @@ -0,0 +1,2 @@ +[config] +project = src\Server\OneTrueError.Web\OneTrueError.Web.csproj diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..7607d65e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,56 @@ +# EditorConfig: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Don't use tabs for indentation. +[*] +indent_style = space +# (Please don't specify an indent_size here; that has too many unintended consequences.) + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# Dotnet code style settings: +[*.cs] +# Sort using and Import directives with System.* appearing first +dotnet_sort_system_directives_first = true + +# Don't use this. qualifier +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion + +# use int x = .. over Int32 +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +# use int.MaxValue over Int32.MaxValue +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Require var all the time. +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Disallow throw expressions. +csharp_style_throw_expression = false:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true diff --git a/.gitattributes b/.gitattributes index ceba70d8..1ff0c423 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,22 +1,63 @@ -# Set the default behavior, in case people don't have core.autocrlf set. +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### * text=auto -# Explicitly declare text files you want to always be normalized and converted -# to native line endings on checkout. -#*.c text -#*.h text +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp -# Declare files that will always have CRLF line endings on checkout. -*.sln text eol=crlf -*.csproj text filter=csprojarrange -*.cs text eol=crlf -*.cshtml text eol=crlf -*.ts text eol=crlf +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary -# Denote all files that are truly binary and should not be modified. -*.png binary -*.jpg binary -*.gif binary -*.obj binary -*.exe binary -*.dll binary +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore index b72cfb21..bcbac429 100644 --- a/.gitignore +++ b/.gitignore @@ -26,10 +26,16 @@ Thumbs.db obj/ [Rr]elease*/ _ReSharper*/ -[Tt]est[Rr]esult* */packages/*/ -src/*/.vs/* -src/*/packages/*/ +**/.vs/* +**/packages/*/ src/Tools/MarkdownToNamespaceDoc/packages/*/ src/Tools/TsGenerator/.vs/* src/Help/* +**/.vs/* +src/Server/Coderr.Server.Web.Tests/applicationhost.config +/src/Server/Coderr.Server.Web/node_modules/* +/src/Server/Coderr.Server.Web/wwwroot/dist/** +/src/Server/Coderr.Server.Web/npm-shrinkwrap.json +/src/Server/node_modules/** +/src/Server/Coderr.Server.WebSite/ClientApp/.angular/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..958489da --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Coderr Server +Copyright (C) 2018 1TCompany AB + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +------------ + +More information is found here: + +https://coderr.io/features/ diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index efbadd1e..00000000 --- a/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -OneTrueError Server -Copyright (C) 2016 Gauffin Interactive AB - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . - ------------- - -More information is found here: - -http://onetrueerror.com/licensing/ diff --git a/ReadMe.md b/ReadMe.md index e871792b..2be6ab99 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -1,22 +1,77 @@ -OneTrueError -================================ +Coderr Community Server +============================= -OneTrueError is an open source error handling service for .NET. It includes the context information that you forgot to include when you logged/reported the exception. +[![Build status](https://1tcompany.visualstudio.com/_apis/public/build/definitions/75570083-b1ef-4e78-88e2-5db4982f756c/6/badge)]() [![Github All Releases](https://img.shields.io/github/downloads/coderrio/coderr.server/total.svg?style=flat-square)]() -![](screenshot.png) +# Solve errors more quickly +It works on my machine! -[Getting started guide](https://www.codeproject.com/articles/1126297/onetrueerror-automated-exception-handling) +We all have heard, and said, that expression. Solving errors can be quite frustrating. Even trivial errors which just takes a few minutes to solve can cause frustration if there are many of them. +Coderr automates the error management process. Let Coderr detect, report and analyze the errors, so that you can focus on just solving them. -## Love or hate our service? +![Welcome screen](docs/discover-incident.png) -Please write a review. +## Search function -* [G2 Crowd](https://www.g2crowd.com/products/onetrueerror/reviews) +![Search using your own data](docs/search.png) +.. don't want to host/maintain your own server? Try [Coderr Cloud](https://lobby.coderr.io/?utm_source=github) - Free up to five users. + +## Getting started + +1. [Download Coderr Server](https://github.com/coderrio/Coderr.Server/releases), use our [cloud service](https://lobby.coderr.io) (free for up to five users) or use our [Docker image]() +2. Install one of our [nuget libraries](https://www.nuget.org/packages?q=coderr.client) (or one of the [js libraries](https://www.npmjs.com/package/coderr.client)). +3. Follow the instructions in the package ReadMe (max three lines of code to get started). +4. Try the code below. + +**Unhandled exceptions will automatically be reported by the client libraries.** + +To report exceptions manually: + +```csharp +public void UpdatePost(int uid, ForumPost post) +{ + try + { + _service.Update(uid, post); + } + catch (Exception ex) + { + Err.Report(ex, new{ UserId = uid, ForumPost = post }); + } +} +``` + +Sample data collected by the ASP.NET Core MVC library: + +![](docs/collections.gif) + +You can learn more about reporting errors [here](https://coderr.io/documentation/getting-started). + +## Running Coderr + +You can run any Coderr in development, test and in production. Coderr is available in three different ways; as Coderr Community Server (AGPL license, self-hosting), as Coderr Cloud (commercial license, cloud version) or on request, as Coderr Premise (commercial license, self-hosting version). Coderr Cloud and Coderr Premise add powerful algorithms to prioritize errors and provide insight to how your code is improving over time with applied solutions. + +[Read more](https://coderr.io/features/) + + +## About us + +We are passionate about Open Source, Microsoft .NET and code quality. 1TCompany started in 2017 in Sweden and builds on years of coding experience and bringing products to market. Our mission is to assist fellow developers deliver quality code. To accomplish this mission, we decided to make Coderr commercially available and ready for prime time. + + +## Community + +* [Discussion board](http://discuss.coderr.io) +* [Report bugs](https://github.com/coderr.io/coderr.server/issues) +* [Documentation](https://coderr.io/documentation) +* [Commercial support](mailto:support@coderr.io?subject=Commercial%20support%20inquiry) ## Licensing -* Server: AGPL -* Client libraries: Apache 2.0 +* Community Server: [AGPL](License) +* Client libraries: [Apache 2.0](https://opensource.org/licenses/apache-2.0) +* [Coderr Cloud](https://lobby.coderr.io): Commercial +* [Coderr Premise](https://coderr.io/features): Commercial diff --git a/CodeOfConduct.md b/docs/CODE_OF_CONDUCT.md similarity index 100% rename from CodeOfConduct.md rename to docs/CODE_OF_CONDUCT.md diff --git a/Contribute.md b/docs/Contributing.md similarity index 100% rename from Contribute.md rename to docs/Contributing.md diff --git a/docs/collections.gif b/docs/collections.gif new file mode 100644 index 00000000..22c90a78 Binary files /dev/null and b/docs/collections.gif differ diff --git a/docs/discover-incident.png b/docs/discover-incident.png new file mode 100644 index 00000000..cafd084d Binary files /dev/null and b/docs/discover-incident.png differ diff --git a/docs/release_banner.png b/docs/release_banner.png new file mode 100644 index 00000000..c359c920 Binary files /dev/null and b/docs/release_banner.png differ diff --git a/docs/search.png b/docs/search.png new file mode 100644 index 00000000..4d5830a1 Binary files /dev/null and b/docs/search.png differ diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 9ba22894..00000000 Binary files a/screenshot.png and /dev/null differ diff --git a/src/DockerCompose/Linux/docker-compose.yml b/src/DockerCompose/Linux/docker-compose.yml new file mode 100644 index 00000000..8dbfafa5 --- /dev/null +++ b/src/DockerCompose/Linux/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3.7" + +services: + + coderr-server-web: + image: "coderrio/coderrserverweb:latest" + container_name: coderr-server-web + hostname: coderr-server-web + ports: + - "2500:80" + networks: + - coderr_network + +volumes: + esdata: + driver: local + +networks: + coderr_network: + name: coderr_network diff --git a/src/DockerCompose/Linux/run example.txt b/src/DockerCompose/Linux/run example.txt new file mode 100644 index 00000000..bceadf67 --- /dev/null +++ b/src/DockerCompose/Linux/run example.txt @@ -0,0 +1 @@ +docker-compose run -p 2500:80 -e "ConnectionStrings:Db"="Data Source=host.docker.internal,1433;Initial Catalog=Coderr99;User Id=sa; Password=Pass@word; Connect Timeout=15;" coderr-server-web \ No newline at end of file diff --git a/src/DockerCompose/Windows/docker-compose.yml b/src/DockerCompose/Windows/docker-compose.yml new file mode 100644 index 00000000..1e638ebb --- /dev/null +++ b/src/DockerCompose/Windows/docker-compose.yml @@ -0,0 +1,29 @@ +version: "3.7" + +services: + + coderr-server-web: + image: "coderrio/communityserver-win:latest" + container_name: coderr_communityserver + hostname: coderr + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=http://+:5000 + + # To enable https, use these lines and configure a certificate + #- ASPNETCORE_URLS=https://+:5001;http://+:5000 + #- ASPNETCORE_HTTPS_PORT=5001 + + # Adjust the connection string + - CODERR_CONNECTIONSTRING=Data Source=host.docker.internal,1433;Initial Catalog=Coderr;Integrated Security=false;Connect Timeout=15;user=coderr;password=c0d3rr + + # Comment out this line once Coderr have been configured (run through the install wizard) + # and then restart the container. + - CODERR_CONFIG_PASSWORD=changeThis + + volumes: + - ${APPDATA}/Microsoft/UserSecrets:C:\ProtectedStorage:rw + + ports: + - "60473:5000/tcp" + - "60474:5001" diff --git a/src/DockerCompose/Windows/run example.txt b/src/DockerCompose/Windows/run example.txt new file mode 100644 index 00000000..0a7f926f --- /dev/null +++ b/src/DockerCompose/Windows/run example.txt @@ -0,0 +1 @@ +docker-compose run -p 2500:80 -e "ConnectionStrings:Db"="Data Source=172.**.**.1,1433;Initial Catalog=Coderr99;User Id=sa; Password=Pass@word; Connect Timeout=15;" coderr-server-web \ No newline at end of file diff --git a/src/Server/.dockerignore b/src/Server/.dockerignore new file mode 100644 index 00000000..c947c5af --- /dev/null +++ b/src/Server/.dockerignore @@ -0,0 +1,34 @@ +# directories +**/bin/ +**/obj/ +**/out/ + +# files +Dockerfile* +#**/*.md + +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/src/Server/Coderr.Server.Abstractions/Boot/ConfigurationContext.cs b/src/Server/Coderr.Server.Abstractions/Boot/ConfigurationContext.cs new file mode 100644 index 00000000..4f905a70 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Boot/ConfigurationContext.cs @@ -0,0 +1,26 @@ +using System; +using System.Data; +using System.Reflection; +using System.Security.Claims; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.Abstractions.Boot +{ + public abstract class ConfigurationContext + { + protected ConfigurationContext(IServiceCollection services, Func serviceProvider) + { + Services = services; + ServiceProvider = serviceProvider; + } + + public Func ConnectionFactory { get; set; } + public IServiceCollection Services { get; private set; } + public Func ServiceProvider { get; private set; } + public IConfiguration Configuration { get; set; } + + public abstract void RegisterMessageTypes(Assembly assembly); + + public abstract ConfigurationContext Clone(IServiceCollection serviceCollection); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Boot/ContainerServiceAttribute.cs b/src/Server/Coderr.Server.Abstractions/Boot/ContainerServiceAttribute.cs new file mode 100644 index 00000000..1a0a61f1 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Boot/ContainerServiceAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Coderr.Server.Abstractions.Boot +{ + public class ContainerServiceAttribute: Attribute + { + public bool IsSingleInstance { get; set; } + public bool IsTransient { get; set; } + public bool RegisterAsSelf { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Abstractions/Boot/IAppModule.cs b/src/Server/Coderr.Server.Abstractions/Boot/IAppModule.cs new file mode 100644 index 00000000..4abb10c3 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Boot/IAppModule.cs @@ -0,0 +1,10 @@ +namespace Coderr.Server.Abstractions.Boot +{ + public interface IAppModule + { + void Configure(ConfigurationContext context); + void Start(StartContext context); + + void Stop(); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Boot/IConfiguration.cs b/src/Server/Coderr.Server.Abstractions/Boot/IConfiguration.cs new file mode 100644 index 00000000..15fc8d69 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Boot/IConfiguration.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Abstractions.Boot +{ + public interface IConfiguration + { + string this[string name] { get; } + IConfigurationSection GetSection(string name); + string GetConnectionString(string name); + IEnumerable GetChildren(); + } + +} diff --git a/src/Server/Coderr.Server.Abstractions/Boot/IConfigurationSection.cs b/src/Server/Coderr.Server.Abstractions/Boot/IConfigurationSection.cs new file mode 100644 index 00000000..cadf5bd0 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Boot/IConfigurationSection.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Abstractions.Boot +{ + /// + /// Abstraction for the .NET Core configuration files. + /// + public interface IConfigurationSection + { + string this[string name] { get; } + IEnumerable GetChildren(); + + string Value { get; } + } + +} diff --git a/src/Server/Coderr.Server.Abstractions/Boot/RegisterExtensions.cs b/src/Server/Coderr.Server.Abstractions/Boot/RegisterExtensions.cs new file mode 100644 index 00000000..7cf4503d --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Boot/RegisterExtensions.cs @@ -0,0 +1,88 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.Abstractions.Boot +{ + public static class RegisterExtensions + { + + public static void RegisterContainerServices(this IServiceCollection serviceCollection, Assembly assembly, [CallerMemberName] string callerName = "") + { + var gotWrongAttribute = ( + from type in assembly.GetTypes() + let attributes = type.GetCustomAttributes() + where attributes.Count(x => x.GetType().FullName == "Griffin.Container.ContainerService") > 0 + select type).ToList(); + if (gotWrongAttribute.Count > 0) + { + throw new InvalidOperationException( + $"Types \'{string.Join(",", gotWrongAttribute)}\' was decorated with the wrong attribute"); + } + + var containerServices = assembly.GetTypes() + .Where(x => x.GetCustomAttribute() != null); + + foreach (var containerService in containerServices) + { + var attr = containerService.GetCustomAttribute(); + var interfaces = containerService.GetInterfaces(); + var lifetime = ConvertLifetime(attr); + + // Hack so that the same instance is resolved for each interface + bool isRegisteredAsSelf = false; + if (interfaces.Length > 1 || attr.RegisterAsSelf) + { + serviceCollection.Add(new ServiceDescriptor(containerService, containerService, lifetime)); + isRegisteredAsSelf = true; + } + + + foreach (var @interface in interfaces) + { + var sd = isRegisteredAsSelf + ? new ServiceDescriptor(@interface, x => x.GetService(containerService), lifetime) // else we don't get the same instance in the scope. + : new ServiceDescriptor(@interface, containerService, lifetime); + serviceCollection.Add(sd); + } + } + } + + public static void RegisterMessageHandlers(this IServiceCollection serviceCollection, Assembly assembly, [CallerMemberName] string callerName = "") + { + var types = assembly.GetTypes() + .Where(y => y.GetInterfaces() + .Any(x => x.Name.Contains("IMessageHandler") || x.Name.Contains("IQueryHandler"))) + .ToList(); + foreach (var type in types) + { + serviceCollection.AddScoped(type, type); + + var ifs = type.GetInterfaces() + .Where(x => x.Name.Contains("IMessageHandler") || x.Name.Contains("IQueryHandler")) + .ToList(); + foreach (var @if in ifs) + { + serviceCollection.AddScoped(@if, x => x.GetService(type)); + } + } + } + + private static ServiceLifetime ConvertLifetime(ContainerServiceAttribute attr) + { + if (attr.IsSingleInstance) + { + return ServiceLifetime.Singleton; + } + + if (attr.IsTransient) + { + return ServiceLifetime.Transient; + } + + return ServiceLifetime.Scoped; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Boot/StartContext.cs b/src/Server/Coderr.Server.Abstractions/Boot/StartContext.cs new file mode 100644 index 00000000..ef029532 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Boot/StartContext.cs @@ -0,0 +1,10 @@ +using System; + +namespace Coderr.Server.Abstractions.Boot +{ + public class StartContext + { + public IServiceProvider ServiceProvider { get; set; } + } + +} diff --git a/src/Server/Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj b/src/Server/Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj new file mode 100644 index 00000000..5306ba1d --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.0 + Debug;Release;Premise + + + + + + + + + + + diff --git a/src/Server/Coderr.Server.Abstractions/Config/ConfigurationCategoryExtensions.cs b/src/Server/Coderr.Server.Abstractions/Config/ConfigurationCategoryExtensions.cs new file mode 100644 index 00000000..36fd5150 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Config/ConfigurationCategoryExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Coderr.Server.Abstractions.Config +{ + /// + /// Extensions used to convert between a flat object and a configuration dictionary + /// + public static class ConfigurationCategoryExtensions + { + /// + /// Assign properties from the configuration dictionary. + /// + /// Category that should get its properties assigned + /// Dictionary containing the property values. + /// section;settings + public static void AssignProperties(this IConfigurationSection section, IDictionary settings) + { + if (section == null) throw new ArgumentNullException("section"); + if (settings == null) throw new ArgumentNullException("settings"); + var type = section.GetType(); + foreach (var kvp in settings) + { + var property = type.GetProperty(kvp.Key); + if (property == null) + continue; + + var propertyType = property.PropertyType; + if (propertyType == typeof(Uri)) + { + var value = new Uri(kvp.Value); + property.SetValue(section, value); + } + else if (!propertyType.IsAssignableFrom(typeof(string))) + { + var realType = Nullable.GetUnderlyingType(propertyType); + if (realType != null) + { + // we got a nullable type and the string represents + // null, so just assign it. + if (string.IsNullOrEmpty(kvp.Value)) + { + property.SetValue(section, null); + continue; + } + + propertyType = realType; + } + + var value = Convert.ChangeType(kvp.Value, propertyType); + property.SetValue(section, value); + } + else + property.SetValue(section, kvp.Value); + } + } + + /// + /// Create a dictionary from the objects properties. + /// + /// Instance to convert + /// Dictionary with all properties (except SectionName) + /// section + public static IDictionary ToConfigDictionary(this IConfigurationSection section) + { + if (section == null) throw new ArgumentNullException("section"); + var items = new Dictionary(); + foreach (var propertyInfo in section.GetType().GetProperties()) + { + if (propertyInfo.Name == "SectionName") + continue; + + var value = propertyInfo.GetValue(section); + items[propertyInfo.Name] = string.Format(CultureInfo.InvariantCulture, "{0}", value); + } + return items; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Config/ConfigurationStore.cs b/src/Server/Coderr.Server.Abstractions/Config/ConfigurationStore.cs new file mode 100644 index 00000000..33a4c889 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Config/ConfigurationStore.cs @@ -0,0 +1,29 @@ +using System; + +namespace Coderr.Server.Abstractions.Config +{ + /// + /// Used to modify configuration settings. + /// + /// + /// + /// Use dependency injection () to access the configuration. + /// + /// + public abstract class ConfigurationStore + { + /// + /// Load a settings section + /// + /// Type of section + /// Category if found; otherwise null. + public abstract T Load() where T : IConfigurationSection, new(); + + /// + /// Store a settings section. + /// + /// Category to persist. + /// section + public abstract void Store(IConfigurationSection section); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Config/IConfiguration.cs b/src/Server/Coderr.Server.Abstractions/Config/IConfiguration.cs new file mode 100644 index 00000000..6f4c4d82 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Config/IConfiguration.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.Abstractions.Config +{ + public interface IConfiguration where TConfigType : new() + { + TConfigType Value { get; } + + void Save(); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Config/IConfigurationSection.cs b/src/Server/Coderr.Server.Abstractions/Config/IConfigurationSection.cs new file mode 100644 index 00000000..b1097f96 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Config/IConfigurationSection.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Abstractions.Config +{ + /// + /// Purpose of this interface is to allow strongly types settings to be stored in a configuration store without + /// exposing magic strings. + /// + public interface IConfigurationSection + { + string SectionName { get; } + + void Load(IDictionary settings); + + IDictionary ToDictionary(); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/HostConfig.cs b/src/Server/Coderr.Server.Abstractions/HostConfig.cs new file mode 100644 index 00000000..f492439d --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/HostConfig.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; + +namespace Coderr.Server.Abstractions +{ + /// + /// Wraps either the configuration file or the Docker environment variables. + /// + public class HostConfig + { + public static HostConfig Instance = new HostConfig(); + + private static bool _isTriggered; + + public bool IsRunningInDocker { get; set; } + public string ConnectionString { get; set; } + + public bool IsDemo { get; set; } = Debugger.IsAttached; + public bool IsConfigured { get; private set; } + + /// + /// Just to make it harder to take over an ongoing installation of Coderr on a public ip. Changed now and then. + /// + public string InstallationPassword { get; set; } + + public event EventHandler Configured; + + public override string ToString() + { + var tmp = IsRunningInDocker ? "[DOCKER] " : "[NATIVE] "; + return $"{tmp} running with {ConnectionString}"; + } + + /// + /// Mark configuration as complete. + /// + public void MarkAsConfigured() + { + IsConfigured = true; + } + + /// + /// Separate method since it must be run after everyone have subscribed on the event. + /// + public void TriggeredConfigured() + { + if (_isTriggered) return; + + _isTriggered = true; + IsConfigured = true; + Configured?.Invoke(this, EventArgs.Empty); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/IConnectionFactory.cs b/src/Server/Coderr.Server.Abstractions/IConnectionFactory.cs new file mode 100644 index 00000000..c97048c0 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/IConnectionFactory.cs @@ -0,0 +1,11 @@ +using System.Data; +using System.Security.Claims; + +namespace Coderr.Server.Abstractions +{ + public interface IConnectionFactory + { + IDbConnection OpenConnection(ClaimsPrincipal principal); + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/IGotTransaction.cs b/src/Server/Coderr.Server.Abstractions/IGotTransaction.cs new file mode 100644 index 00000000..299542b0 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/IGotTransaction.cs @@ -0,0 +1,9 @@ +using System.Data.Common; + +namespace Coderr.Server.Abstractions +{ + public interface IGotTransaction + { + DbTransaction Transaction { get; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/IPAddressExtension.cs b/src/Server/Coderr.Server.Abstractions/IPAddressExtension.cs new file mode 100644 index 00000000..f82ee282 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/IPAddressExtension.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Text; + +namespace Coderr.Server.Abstractions +{ + /// + /// Extensions for IPAddress. + /// + public static class IpAddressExtensions + { + /// + /// An extension method to determine if an IP address is internal, as specified in RFC1918 + /// + /// The IP address that will be tested + /// Returns true if the IP is internal, false if it is external + public static bool IsInternal(this IPAddress toTest) + { + if (IPAddress.IsLoopback(toTest)) return true; + + if (toTest.IsIPv6LinkLocal || toTest.IsIPv6SiteLocal) + { + return true; + } + + var bytes = toTest.GetAddressBytes(); + switch (bytes[0]) + { + case 10: + return true; + case 172: + return bytes[1] < 32 && bytes[1] >= 16; + case 192: + return bytes[1] == 168; + default: + return false; + } + } + } +} diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/HighlightedContextDataProviderContext.cs b/src/Server/Coderr.Server.Abstractions/Incidents/HighlightedContextDataProviderContext.cs new file mode 100644 index 00000000..cc43999d --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Incidents/HighlightedContextDataProviderContext.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.Api.Core.Incidents.Queries; + +namespace Coderr.Server.Abstractions.Incidents +{ + public class HighlightedContextDataProviderContext + { + private readonly IList _items; + + public HighlightedContextDataProviderContext(IList items) + { + _items = items ?? throw new ArgumentNullException(nameof(items)); + Tags = new string[0]; + } + + public int ApplicationId { get; set; } + public string Description { get; set; } + + /// + /// Namespace + name of exception + /// + public string FullName { get; set; } + + public int IncidentId { get; set; } + + public IEnumerable Items => _items; + + public string StackTrace { get; set; } + public string[] Tags { get; set; } + + public void AddValue(HighlightedContextData contextData) + { + if (contextData == null) throw new ArgumentNullException(nameof(contextData)); + _items.Add(contextData); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/IHighlightedContextDataProvider.cs b/src/Server/Coderr.Server.Abstractions/Incidents/IHighlightedContextDataProvider.cs new file mode 100644 index 00000000..d7e7ee47 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Incidents/IHighlightedContextDataProvider.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Queries; + +namespace Coderr.Server.Abstractions.Incidents +{ + public interface IHighlightedContextDataProvider + { + Task CollectAsync(HighlightedContextDataProviderContext context); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/IQuickfactProvider.cs b/src/Server/Coderr.Server.Abstractions/Incidents/IQuickfactProvider.cs new file mode 100644 index 00000000..b3724fa1 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Incidents/IQuickfactProvider.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Coderr.Server.Abstractions.Incidents +{ + public interface IQuickfactProvider + { + Task CollectAsync(QuickFactContext context); + } +} diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/ISolutionProvider.cs b/src/Server/Coderr.Server.Abstractions/Incidents/ISolutionProvider.cs new file mode 100644 index 00000000..00e2e0f6 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Incidents/ISolutionProvider.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Queries; + +namespace Coderr.Server.Abstractions.Incidents +{ + /// + /// Checks if there is a solution available for the current incident. + /// + public interface ISolutionProvider + { + Task SuggestSolutionAsync(SolutionProviderContext context); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/QuickFactContext.cs b/src/Server/Coderr.Server.Abstractions/Incidents/QuickFactContext.cs new file mode 100644 index 00000000..fa8812cb --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Incidents/QuickFactContext.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.Api.Core.Incidents.Queries; + +namespace Coderr.Server.Abstractions.Incidents +{ + public class QuickFactContext + { + public QuickFactContext(int applicationId, int incidentId, ICollection facts) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + ApplicationId = applicationId; + IncidentId = incidentId; + CollectedFacts = facts; + } + public int IncidentId { get; private set; } + public int ApplicationId { get; private set; } + public ICollection CollectedFacts { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Incidents/SolutionProviderContext.cs b/src/Server/Coderr.Server.Abstractions/Incidents/SolutionProviderContext.cs new file mode 100644 index 00000000..f69a78df --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Incidents/SolutionProviderContext.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.Api.Core.Incidents.Queries; + +namespace Coderr.Server.Abstractions.Incidents +{ + public class SolutionProviderContext + { + private readonly List _possibleSolutions; + + public SolutionProviderContext(List possibleSolutions) + { + _possibleSolutions = possibleSolutions; + } + + public int ApplicationId { get; set; } + public string Description { get; set; } + + /// + /// Namespace + name of exception + /// + public string FullName { get; set; } + + public int IncidentId { get; set; } + + public string StackTrace { get; set; } + public string[] Tags { get; set; } + + public void AddSuggestion(string suggestion, string motivation) + { + if (suggestion == null) throw new ArgumentNullException(nameof(suggestion)); + if (motivation == null) throw new ArgumentNullException(nameof(motivation)); + _possibleSolutions.Add(new SuggestedIncidentSolution {Reason = motivation, SuggestedSolution = suggestion}); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/QueueConfig.cs b/src/Server/Coderr.Server.Abstractions/QueueConfig.cs new file mode 100644 index 00000000..c1900f6e --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/QueueConfig.cs @@ -0,0 +1,36 @@ +using System; +using Coderr.Server.Abstractions.Boot; + +namespace Coderr.Server.Abstractions +{ + public class QueueConfig + { + public string ReportQueue { get; set; } + public string InboundPartitions { get; set; } + public string ReportEventQueue { get; set; } + public string AppQueue { get; set; } + + public void Configure(IConfiguration config) + { + if (config == null) + { + throw new ArgumentNullException(nameof(config)); + } + + if (ServerConfig.Instance.IsLive) + { + ReportQueue = config.GetSection("MessageQueue")["ReportQueue"]; + ReportEventQueue = config.GetSection("MessageQueue")["ReportEventQueue"]; + AppQueue = config.GetSection("MessageQueue")["AppQueue"]; + InboundPartitions = config.GetSection("MessageQueue")["InboundPartitions"]; + } + else + { + ReportQueue = "ErrorReports"; + ReportEventQueue = "ErrorReportEvents"; + AppQueue = "Messaging"; + InboundPartitions = "InboundPartitions"; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Reports/ReportConfig.cs b/src/Server/Coderr.Server.Abstractions/Reports/ReportConfig.cs new file mode 100644 index 00000000..7a451f63 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Reports/ReportConfig.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using Coderr.Server.Abstractions.Config; + +namespace Coderr.Server.Abstractions.Reports +{ + /// + /// Configuration settings for reports. + /// + public class ReportConfig : IConfigurationSection//, IReportConfig + { + /// + /// Creates a new instance of + /// + /// + /// + /// Sets default of MaxReportJsonSize to 2000000, MaxReportsPerIncident to 25 and RetentionDays to 90. + /// + /// + public ReportConfig() + { + MaxReportJsonSize = 2000000; + MaxReportsPerIncident = 25; + RetentionDays = 90; + } + + /// + /// Maximum number of bytes that a uncompressed JSON report can be + /// + /// + /// + /// Used to filter out reports that are too large. + /// + /// + public int MaxReportJsonSize { get; set; } + + /// + /// Max number of reports per incident + /// + /// + /// The oldest report(s) will be deleted if this limit is reached. + /// + public int MaxReportsPerIncident { get; set; } + + /// + /// Number of days to store reports. + /// + /// + /// All reports older than this amount of days will be deleted. + /// + public int RetentionDays { get; set; } + + /// + /// Number of days to store incidents that have not received any new reports. + /// + public int RetentionDaysIncidents { get; set; } + + string IConfigurationSection.SectionName => "ReportConfig"; + + IDictionary IConfigurationSection.ToDictionary() + { + return this.ToConfigDictionary(); + } + + void IConfigurationSection.Load(IDictionary settings) + { + this.AssignProperties(settings); + if (MaxReportJsonSize == 0) + MaxReportJsonSize = 1000000; + if (RetentionDaysIncidents == 0) + RetentionDaysIncidents = 90; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Security/ClaimsExtensions.cs b/src/Server/Coderr.Server.Abstractions/Security/ClaimsExtensions.cs new file mode 100644 index 00000000..8527dd08 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Security/ClaimsExtensions.cs @@ -0,0 +1,189 @@ +using System; +using System.Security.Authentication; +using System.Security.Claims; +using System.Security.Principal; + +namespace Coderr.Server.Abstractions.Security +{ + /// + /// Our Coderr specific extensions for claims handling. + /// + public static class ClaimsExtensions + { + /// + /// Throws if returns false. + /// + /// principal to search in + /// application to check + /// true if the claim was found with the given value; otherwise false. + /// Claim is not found in the identity. + public static void EnsureApplicationAdmin(this ClaimsPrincipal principal, int applicationId) + { + if (!IsApplicationAdmin(principal, applicationId)) + throw new UnauthorizedAccessException( + $"User {principal.Identity.Name} is not authorized to manage application {applicationId}."); + } + + /// + /// Get account id (). + /// + /// principal to search in + /// id + /// Claim is not found in the identity/ies. + public static int GetAccountId(this ClaimsPrincipal principal) + { + var claim = principal.FindFirst(x => x.Type == ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException( + "Failed to find ClaimTypes.NameIdentifier, user is probably not logged in."); + + if (int.TryParse(claim.Value, out var userId)) return userId; + + var ex = new FormatException($"UserId {claim.Value} is not a number."); + foreach (var c in principal.Claims) + { + ex.Data[$"Claim.{c.Type}"] = c.Value; + } + + throw ex; + } + + /// + /// Get account id (). + /// + /// principal to search in + /// id + /// Claim is not found in the identity/ies. + public static int FindAccountId(this ClaimsPrincipal principal) + { + var claim = principal.FindFirst(x => x.Type == ClaimTypes.NameIdentifier); + if (claim == null) + return 0; + + if (int.TryParse(claim.Value, out var userId)) + return userId; + + return -1; + } + + /// + /// Get account id (). + /// + /// principal to search in + /// id + /// Claim is not found in the identity/ies. + public static int GetAccountId(this IPrincipal principal) + { + var prince = principal as ClaimsPrincipal; + if (prince == null) + throw new AuthenticationException(principal + " is not a ClaimsPrincipal."); + + var claim = prince.FindFirst(x => x.Type == ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException( + "Failed to find ClaimTypes.NameIdentifier, user is probably not logged in."); + + return int.Parse(claim.Value); + } + + /// + /// Get account id (). + /// + /// identity to search in + /// id + /// Claim is not found in the identity. + public static int GetAccountId(this ClaimsIdentity identity) + { + var claim = identity.FindFirst(x => x.Type == ClaimTypes.NameIdentifier); + if (claim == null) + throw new InvalidOperationException( + "Failed to find ClaimTypes.NameIdentifier, user is probably not logged in."); + + return int.Parse(claim.Value); + } + + /// + /// Checks if the currently logged in user is the same as the given id. + /// + /// Some kind of principal + /// AccountId to compare with. + /// + /// true if current principal is a ClaimsPrincipal, the user is authenticated and the accountId is + /// same; otherwise false. + /// + public static bool IsCurrentAccount(this IPrincipal principal, int accountId) + { + var p = principal as ClaimsPrincipal; + if (p == null) + return false; + + if (!p.Identity.IsAuthenticated) + return false; + + return accountId == p.GetAccountId(); + } + + /// + /// Checks if the user has the CoderrClaims.ApplicationAdmin claim or if user is SysAdmin or System. + /// + /// principal to search in + /// Application to check + /// true if the claim was found with the given value; otherwise false. + /// Claim is not found in the identity. + public static bool IsApplicationAdmin(this ClaimsPrincipal principal, int applicationId) + { + return principal.HasClaim(CoderrClaims.ApplicationAdmin, applicationId.ToString()) + || principal.IsInRole(CoderrRoles.SysAdmin) + || principal.IsInRole(CoderrRoles.System); + } + + /// + /// Get if the CoderrClaims.Application claim is specified for the given application (claim value) + /// + /// principal to search in + /// App to check + /// Check if user is in role SysAdmin or if the user is the System. + /// true if the claim was found with the given value; otherwise false. + /// Claim is not found in the identity. + public static bool IsApplicationMember(this ClaimsPrincipal principal, int applicationId, bool checkSystemRoles = false) + { + var isAdmin = principal.HasClaim(CoderrClaims.Application, applicationId.ToString()); + if (checkSystemRoles) + isAdmin = isAdmin || IsSysAdmin(principal) || principal.IsInRole(CoderrRoles.System); + return isAdmin; + } + + /// + /// Get if the user is part of CoderrRoles.SysAdmin. + /// + /// principal to search in + /// true if the role was found; otherwise false. + public static bool IsSysAdmin(this IPrincipal principal) + { + return principal.IsInRole(CoderrRoles.SysAdmin); + } + + public static string ToFriendlyString(this IPrincipal principal) + { + var cc = principal as ClaimsPrincipal; + if (cc == null || principal.Identity?.IsAuthenticated != true) + { + return "Anonymous"; + } + + string str = cc.Identity.Name + " Claims["; + foreach (var claim in cc.Claims) + { + var pos = claim.Type.LastIndexOf('/'); + if (pos != -1) + str += claim.Type.Substring(pos + 1); + else + str += claim.Type; + + str += "=" + claim.Value + ", "; + } + str = str.Remove(str.Length - 2, 2) + "]"; + return str; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Security/CoderrClaims.cs b/src/Server/Coderr.Server.Abstractions/Security/CoderrClaims.cs new file mode 100644 index 00000000..5194afe3 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Security/CoderrClaims.cs @@ -0,0 +1,17 @@ +using System.Security.Claims; + +namespace Coderr.Server.Abstractions.Security +{ + public class CoderrClaims + { + public const string Application = "http://coderr/claims/application"; + public const string ApplicationAdmin = "http://coderr/claims/application/admin"; + + public static readonly ClaimsPrincipal SystemPrincipal = new ClaimsPrincipal(new ClaimsIdentity(new[] + { + new Claim(ClaimTypes.NameIdentifier, "0"), + new Claim(ClaimTypes.Name, "System"), + new Claim(ClaimTypes.Role, CoderrRoles.System) + })); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Security/CoderrRoles.cs b/src/Server/Coderr.Server.Abstractions/Security/CoderrRoles.cs new file mode 100644 index 00000000..7478c090 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Security/CoderrRoles.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.Abstractions.Security +{ + public class CoderrRoles + { + public const string SysAdmin = "SysAdmin"; + public const string User = "User"; + public const string System = "System"; + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Abstractions/Security/IPrincipalAccessor.cs b/src/Server/Coderr.Server.Abstractions/Security/IPrincipalAccessor.cs new file mode 100644 index 00000000..26499018 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/Security/IPrincipalAccessor.cs @@ -0,0 +1,14 @@ +using System.Security.Claims; + +namespace Coderr.Server.Abstractions.Security +{ + /// + /// Used to move principal from the queue to the current container scope so that a proper DB connection can be opened. + /// + public interface IPrincipalAccessor + { + ClaimsPrincipal Principal { get; set; } + + ClaimsPrincipal FindPrincipal(); + } +} diff --git a/src/Server/Coderr.Server.Abstractions/ServerConfig.cs b/src/Server/Coderr.Server.Abstractions/ServerConfig.cs new file mode 100644 index 00000000..4a974dd3 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/ServerConfig.cs @@ -0,0 +1,47 @@ +using System; + +namespace Coderr.Server.Abstractions +{ + public class ServerConfig + { + public const string Premise = "Premise"; + public const string Live = "Live"; + + public static ServerConfig Instance = new ServerConfig(); + private ServerType _serverType; + + public bool IsLive { get; private set; } + + public bool UseSmtpHandler { get; set; } + + public ServerType ServerType + { + get => _serverType; + set + { + _serverType = value; + IsLive = value == ServerType.Live; + UseSmtpHandler = !IsLive; + } + } + + public QueueConfig Queues { get; set; } = new QueueConfig(); + public bool IsCommercial { get; set; } + + public bool IsModuleIgnored(Type type) + { + if (Instance.IsLive) + { + if (type.Assembly.GetName().Name.Contains($".{ServerConfig.Premise}")) + return true; + } + else + { + if (type.Assembly.GetName().Name.Contains($".{ServerConfig.Live}")) + return true; + } + + return false; + } + } +} diff --git a/src/Server/Coderr.Server.Abstractions/ServerType.cs b/src/Server/Coderr.Server.Abstractions/ServerType.cs new file mode 100644 index 00000000..3fb41169 --- /dev/null +++ b/src/Server/Coderr.Server.Abstractions/ServerType.cs @@ -0,0 +1,13 @@ +using System; + +namespace Coderr.Server.Abstractions +{ + [Flags] + public enum ServerType + { + Community = 0, + Premise = 1, + PremisePlus = 3, + Live = 4 + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api.Client.Tests/Coderr.Server.Api.Client.Tests.csproj b/src/Server/Coderr.Server.Api.Client.Tests/Coderr.Server.Api.Client.Tests.csproj new file mode 100644 index 00000000..b14c5c9f --- /dev/null +++ b/src/Server/Coderr.Server.Api.Client.Tests/Coderr.Server.Api.Client.Tests.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.1 + Coderr.Server.Api.Client.Tests + Coderr.Server.Api.Client.Tests + Debug;Release;Premise + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api.Client.Tests/TryTheClient.cs b/src/Server/Coderr.Server.Api.Client.Tests/TryTheClient.cs new file mode 100644 index 00000000..b9b7bf2c --- /dev/null +++ b/src/Server/Coderr.Server.Api.Client.Tests/TryTheClient.cs @@ -0,0 +1,31 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Queries; +using FluentAssertions; + +namespace Coderr.Server.Api.Client.Tests +{ +#if DEBUG + public class TryTheClient + { + //[Fact] + public async Task Test() + { + var client = new ServerApiClient(); + client.Open(new Uri("http://localhost/coderr/"), "", ""); + FindAccountByUserNameResult result = null; + try + { + result = await client.QueryAsync(new FindAccountByUserName("admin")); + } + catch (WebException) + { + } + + + result.Should().NotBeNull(); + } + } +#endif +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api.Client/Coderr.Server.Api.Client.csproj b/src/Server/Coderr.Server.Api.Client/Coderr.Server.Api.Client.csproj new file mode 100644 index 00000000..83788da5 --- /dev/null +++ b/src/Server/Coderr.Server.Api.Client/Coderr.Server.Api.Client.csproj @@ -0,0 +1,41 @@ + + + netstandard2.0 + 2.1.5 + true + bin\$(Configuration)\$(TargetFramework)\Coderr.Server.Api.Client.xml + Debug;Release;Premise + + + Coderr.Server.ApiClient + 1TCompany AB + API client for Coderr Server. + true + Switched to HttpClient + Copyright 2019 © 1TCompany AB. All rights reserved. + logger exceptions analysis .net-core netstandard + https://coderr.io/images/nuget_icon.png + https://github.com/coderrio/coderr.server + git + Apache-2.0 + https://coderr.io + + + + + + + + + + + diff --git a/src/Server/Coderr.Server.Api.Client/Json/IncludeNonPublicMembersContractResolver.cs b/src/Server/Coderr.Server.Api.Client/Json/IncludeNonPublicMembersContractResolver.cs new file mode 100644 index 00000000..430a91d5 --- /dev/null +++ b/src/Server/Coderr.Server.Api.Client/Json/IncludeNonPublicMembersContractResolver.cs @@ -0,0 +1,37 @@ +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Coderr.Server.Api.Client.Json +{ + /// + /// Used by JSON.NET to be able to deserialize properties with private setters. + /// + public class IncludeNonPublicAndUseCamelCaseContractResolver : CamelCasePropertyNamesContractResolver + { + //protected override List GetSerializableMembers(Type objectType) + //{ + // var members = base.GetSerializableMembers(objectType); + // return members.Where(m => !m.Name.EndsWith("k__BackingField")).ToList(); + //} + + /// + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + //TODO: Maybe cache + var prop = base.CreateProperty(member, memberSerialization); + + if (!prop.Writable) + { + var property = member as PropertyInfo; + if (property != null) + { + var hasPrivateSetter = property.GetSetMethod(true) != null; + prop.Writable = hasPrivateSetter; + } + } + + return prop; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api.Client/ServerApiClient.cs b/src/Server/Coderr.Server.Api.Client/ServerApiClient.cs new file mode 100644 index 00000000..85e97c9d --- /dev/null +++ b/src/Server/Coderr.Server.Api.Client/ServerApiClient.cs @@ -0,0 +1,143 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Coderr.Server.Api.Client.Json; +using DotNetCqs; +using Newtonsoft.Json; + +namespace Coderr.Server.Api.Client +{ + /// + /// Client for the Coderr server API + /// + public class ServerApiClient : IMessageBus, IQueryBus + { + private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings + { + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + ContractResolver = new IncludeNonPublicAndUseCamelCaseContractResolver() + }; + + private string _apiKey; + private string _sharedSecret; + private Uri _uri; + + + /// + /// Send a query to the server. + /// + /// Principal for the user making the request + /// Message being sent + /// task + async Task IMessageBus.SendAsync(ClaimsPrincipal principal, object message) + { + await RequestAsync(HttpMethod.Post, "send", message); + } + + async Task IMessageBus.SendAsync(ClaimsPrincipal principal, Message message) + { + await RequestAsync(HttpMethod.Post, "send", message.Body); + } + + async Task IMessageBus.SendAsync(Message message) + { + await RequestAsync(HttpMethod.Post, "send", message.Body); + } + + /// + /// Send a command or event + /// + /// message + /// task + public async Task SendAsync(object message) + { + await RequestAsync(HttpMethod.Post, "send", message); + } + + async Task IQueryBus.QueryAsync(ClaimsPrincipal user, Query query) + { + //TODO: Unwrap the cqs object to query parameters instead + //to allow caching in the server + var response = await RequestAsync(HttpMethod.Post, "query", query); + if (response.StatusCode == HttpStatusCode.NotFound) + return default(TResult); + return await DeserializeResponse(response.Content); + } + + /// + /// Make a query + /// + /// Type of result that the query returns + /// query to invoke + /// task + public async Task QueryAsync(Query query) + { + //TODO: Unwrap the cqs object to query parameters instead + //to allow caching in the server + var response = await RequestAsync(HttpMethod.Post, "query", query); + if (response.StatusCode == HttpStatusCode.NotFound) + return default(TResult); + response.EnsureSuccessStatusCode(); + return await DeserializeResponse(response.Content); + } + + + /// + /// Open a channel + /// + /// Root URL to the Coderr web + /// API key from the administration area in Coderr web + /// Shared secret from the administration area in Coderr web + public void Open(Uri uri, string apiKey, string sharedSecret) + { + _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); + _sharedSecret = sharedSecret ?? throw new ArgumentNullException(nameof(sharedSecret)); + _uri = uri ?? throw new ArgumentNullException(nameof(uri)); + } + + + private async Task DeserializeResponse(HttpContent content) + { + var jsonStr = await content.ReadAsStringAsync(); + try + { + var responseObj = JsonConvert.DeserializeObject(jsonStr, typeof(TResult), _jsonSerializerSettings); + return (TResult) responseObj; + } + catch (Exception ex) + { + throw new InvalidOperationException("Failed to deserialize " + jsonStr, ex); + } + } + + private async Task RequestAsync(HttpMethod httpMethod, string cqsType, object cqsObject) + { + var request = new HttpRequestMessage(httpMethod, $"{_uri}api/cqs"); + request.Headers.Add("X-Api-Key", _apiKey); + request.Headers.Add("X-Cqs-Name", cqsObject.GetType().Name); + + var json = JsonConvert.SerializeObject(cqsObject, _jsonSerializerSettings); + var buffer = Encoding.UTF8.GetBytes(json); + var hamc = new HMACSHA256(Encoding.UTF8.GetBytes(_sharedSecret.ToLower())); + var hash = hamc.ComputeHash(buffer); + var signature = Convert.ToBase64String(hash); + request.Headers.Add("Authorization", "ApiKey " + _apiKey + " " + signature); + request.Headers.Add("X-Api-Signature", signature); + + request.Content = new ByteArrayContent(buffer); + + var client = new HttpClient(); + var response = await client.SendAsync(request); + if ((int)response.StatusCode >= 500) + throw new HttpRequestException(response.ReasonPhrase); + return response; + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client/StringBuilderExtensions.cs b/src/Server/Coderr.Server.Api.Client/StringBuilderExtensions.cs similarity index 92% rename from src/Server/OneTrueError.Api.Client/StringBuilderExtensions.cs rename to src/Server/Coderr.Server.Api.Client/StringBuilderExtensions.cs index 40eb8b9c..d201b06b 100644 --- a/src/Server/OneTrueError.Api.Client/StringBuilderExtensions.cs +++ b/src/Server/Coderr.Server.Api.Client/StringBuilderExtensions.cs @@ -1,7 +1,7 @@ using System; using System.Text; -namespace OneTrueError.Api.Client +namespace Coderr.Server.Api.Client { internal static class StringBuilderExtensions { diff --git a/src/Server/OneTrueError.Api/AuthorizeAttribute.cs b/src/Server/Coderr.Server.Api/AuthorizeAttribute.cs similarity index 89% rename from src/Server/OneTrueError.Api/AuthorizeAttribute.cs rename to src/Server/Coderr.Server.Api/AuthorizeAttribute.cs index 8c9d50e7..548216ae 100644 --- a/src/Server/OneTrueError.Api/AuthorizeAttribute.cs +++ b/src/Server/Coderr.Server.Api/AuthorizeAttribute.cs @@ -1,27 +1,26 @@ -using System; -using OneTrueError.Api.Core; - -namespace OneTrueError.Api -{ - /// - /// Authorize on specific roles. - /// - [AttributeUsage(AttributeTargets.Class), IgnoreField] - public class AuthorizeRolesAttribute : Attribute - { - /// - /// Creates a new instance of . - /// - /// roles granted access - public AuthorizeRolesAttribute(params string[] roles) - { - if (roles == null) throw new ArgumentNullException("roles"); - Roles = roles; - } - - /// - /// Roles granted access - /// - public string[] Roles { get; private set; } - } +using System; + +namespace Coderr.Server.Api +{ + /// + /// Authorize on specific roles. + /// + [AttributeUsage(AttributeTargets.Class), IgnoreField] + public class AuthorizeRolesAttribute : Attribute + { + /// + /// Creates a new instance of . + /// + /// roles granted access + public AuthorizeRolesAttribute(params string[] roles) + { + if (roles == null) throw new ArgumentNullException("roles"); + Roles = roles; + } + + /// + /// Roles granted access + /// + public string[] Roles { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Coderr.Server.Api.csproj b/src/Server/Coderr.Server.Api/Coderr.Server.Api.csproj new file mode 100644 index 00000000..c9de5edd --- /dev/null +++ b/src/Server/Coderr.Server.Api/Coderr.Server.Api.csproj @@ -0,0 +1,31 @@ + + + netstandard2.0 + 2.1.5 + bin\$(Configuration)\$(TargetFramework)\Coderr.Server.Api.xml + Added the whitelist API + + + Coderr.Server.Api + Coderr.Server.Api + true + Coderr.Server.Api + Coderr AB + API definition for Coderr Server. + true + Copyright 2020 © Coderr AB. All rights reserved. + logger exceptions analysis .net-core netstandard + https://coderr.io/images/nuget_icon.png + https://github.com/coderrio/coderr.server + git + Apache-2.0 + https://coderr.io + + + 1701;1702;1705;1591 + + + + + + diff --git a/src/Server/Coderr.Server.Api/CommandAttribute.cs b/src/Server/Coderr.Server.Api/CommandAttribute.cs new file mode 100644 index 00000000..651b697e --- /dev/null +++ b/src/Server/Coderr.Server.Api/CommandAttribute.cs @@ -0,0 +1,10 @@ +namespace Coderr.Server.Api +{ + /// + /// Marker attribute to tell which DTOs are commands + /// + public class CommandAttribute : MessageAttribute + { + + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Commands/DeclineInvitation.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Commands/DeclineInvitation.cs similarity index 86% rename from src/Server/OneTrueError.Api/Core/Accounts/Commands/DeclineInvitation.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Commands/DeclineInvitation.cs index c63a0cd4..244ae214 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Commands/DeclineInvitation.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Commands/DeclineInvitation.cs @@ -1,34 +1,34 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Commands -{ - /// - /// Invited person do not want to accept the invitation - /// - public class DeclineInvitation : Command - { - /// - /// Serialization constructor - /// - protected DeclineInvitation() - { - } - - /// - /// Creates a new instance of . - /// - /// invitation key (typically a guid) - public DeclineInvitation(string invitationId) - { - if (string.IsNullOrEmpty(invitationId)) - throw new ArgumentException("Argument is null or empty", "invitationId"); - InvitationId = invitationId; - } - - /// - /// Invitation key (typically a guid) - /// - public string InvitationId { get; private set; } - } +using System; + +namespace Coderr.Server.Api.Core.Accounts.Commands +{ + /// + /// Invited person do not want to accept the invitation + /// + [Message] + public class DeclineInvitation + { + /// + /// Serialization constructor + /// + protected DeclineInvitation() + { + } + + /// + /// Creates a new instance of . + /// + /// invitation key (typically a guid) + public DeclineInvitation(string invitationId) + { + if (string.IsNullOrEmpty(invitationId)) + throw new ArgumentException("Argument is null or empty", "invitationId"); + InvitationId = invitationId; + } + + /// + /// Invitation key (typically a guid) + /// + public string InvitationId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Commands/RegisterAccount.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Commands/RegisterAccount.cs new file mode 100644 index 00000000..4afce887 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Commands/RegisterAccount.cs @@ -0,0 +1,86 @@ +using System; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + +namespace Coderr.Server.Api.Core.Accounts.Commands +{ + /// + /// Register a new account and send out an activation email. + /// + [Message] + public class RegisterAccount + { + /// + /// Creates a new instance of + /// + /// User name + /// Password as entered by the user + /// Email address + public RegisterAccount(string userName, string password, string email) + { + UserName = userName ?? throw new ArgumentNullException("userName"); + Password = password ?? throw new ArgumentNullException("password"); + Email = email ?? throw new ArgumentNullException("email"); + } + + /// + /// Serialization constructor. + /// + protected RegisterAccount() + { + } + + /// + /// Use a specific account id + /// + /// + /// 0 = auto increment + /// + public int AccountId { get; private set; } + + /// + /// do not send an activation email, activate the account directly. + /// + public bool ActivateDirectly { get; private set; } + + /// + /// Email address. + /// + public string Email { get; private set; } + + /// + /// Password as entered by the user. + /// + public string Password { get; private set; } + + /// + /// User name + /// + public string UserName { get; private set; } + + /// + /// The the registration was due to an invitation, we need to redirect back to that invite. + /// + public string ReturnUrl { get; set; } + + /// + /// Activate this account directly + /// + /// Id of the account + public void Activate(int accountId) + { + if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); + ActivateDirectly = true; + AccountId = accountId; + } + + /// + /// Activate this account directly + /// + public void Activate() + { + ActivateDirectly = true; + AccountId = 0; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Commands/RequestPasswordReset.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Commands/RequestPasswordReset.cs new file mode 100644 index 00000000..1292b360 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Commands/RequestPasswordReset.cs @@ -0,0 +1,33 @@ +using System; + +namespace Coderr.Server.Api.Core.Accounts.Commands +{ + /// + /// Request a password reset (i.e. lock account, email an activation link to the user and wait for activation). + /// + [Message] + public class RequestPasswordReset + { + /// + /// Serialization constructor + /// + protected RequestPasswordReset() + { + } + + /// + /// Create a new instance of . + /// + /// Email address associated with the user account. + public RequestPasswordReset(string emailAddress) + { + if (emailAddress == null) throw new ArgumentNullException("emailAddress"); + EmailAddress = emailAddress; + } + + /// + /// Email address associated with the user account. + /// + public string EmailAddress { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Events/AccountActivated.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Events/AccountActivated.cs similarity index 83% rename from src/Server/OneTrueError.Api/Core/Accounts/Events/AccountActivated.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Events/AccountActivated.cs index b2a4f8d2..0852358b 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Events/AccountActivated.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Events/AccountActivated.cs @@ -1,46 +1,46 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Events -{ - /// - /// Published when the user have clicked on the activation link in the registration email. - /// - public class AccountActivated : ApplicationEvent - { - /// - /// Creates a new instance of - - /// - /// Primary key for the created account - /// username that the account was created with. - public AccountActivated(int accountId, string userName) - { - if (userName == null) throw new ArgumentNullException("userName"); - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - AccountId = accountId; - UserName = userName; - } - - /// - /// Serialization constructor. - /// - protected AccountActivated() - { - } - - /// - /// Primary key for the account - /// - public int AccountId { get; set; } - - /// - /// Email address associated with the account. - /// - public string EmailAddress { get; set; } - - /// - /// Unique user name - /// - public string UserName { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Accounts.Events +{ + /// + /// Published when the user have clicked on the activation link in the registration email. + /// + [Message] + public class AccountActivated + { + /// + /// Creates a new instance of - + /// + /// Primary key for the created account + /// user name that the account was created with. + public AccountActivated(int accountId, string userName) + { + if (userName == null) throw new ArgumentNullException("userName"); + if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); + AccountId = accountId; + UserName = userName; + } + + /// + /// Serialization constructor. + /// + protected AccountActivated() + { + } + + /// + /// Primary key for the account + /// + public int AccountId { get; set; } + + /// + /// Email address associated with the account. + /// + public string EmailAddress { get; set; } + + /// + /// Unique user name + /// + public string UserName { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Events/AccountRegistered.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Events/AccountRegistered.cs new file mode 100644 index 00000000..ab78462e --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Events/AccountRegistered.cs @@ -0,0 +1,54 @@ +using System; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + +namespace Coderr.Server.Api.Core.Accounts.Events +{ + /// + /// An user have registered an account and activated it. + /// + [Message] + public class AccountRegistered + { + /// + /// Create a new instance of - + /// + /// Account id (primary key). + /// User name as entered by the user. + public AccountRegistered(int accountId, string userName) + { + if (accountId <= 0) throw new ArgumentNullException("accountId"); + AccountId = accountId; + UserName = userName ?? throw new ArgumentNullException(nameof(userName)); + } + + /// + /// Serialization constructor + /// + protected AccountRegistered() + { + } + + /// + /// Account id (primary key). + /// + + public int AccountId { get; private set; } + + /// + /// The registered user is a system administrator + /// + /// + /// + /// System administrators can create new applications, decide who is application administrator + /// and configure other system wide settings. + /// + /// + public bool IsSysAdmin { get; set; } + + /// + /// User name as entered by the user. + /// + public string UserName { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Events/InvitationAccepted.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Events/InvitationAccepted.cs similarity index 89% rename from src/Server/OneTrueError.Api/Core/Accounts/Events/InvitationAccepted.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Events/InvitationAccepted.cs index a64d7067..1d0ddb26 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Events/InvitationAccepted.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Events/InvitationAccepted.cs @@ -1,66 +1,66 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Events -{ - /// - /// A user have accepted an invitation. - /// - public class InvitationAccepted : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// account that accepted the inviation - /// user that made the invite - /// userName of the person that accepted the invitation - /// invitedByUserName; userName - /// accountId - public InvitationAccepted(int accountId, string invitedByUserName, string userName) - { - if (invitedByUserName == null) throw new ArgumentNullException("invitedByUserName"); - if (userName == null) throw new ArgumentNullException("userName"); - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - AccountId = accountId; - InvitedByUserName = invitedByUserName; - UserName = userName; - } - - /// - /// Serialization constructor. - /// - protected InvitationAccepted() - { - } - - /// - /// The email that the inviation was accepted by. - /// - public string AcceptedEmailAddress { get; set; } - - /// - /// Id of the user that accepted the invitation - /// - public int AccountId { get; set; } - - /// - /// Applications that the user got access to. - /// - public int[] ApplicationIds { get; set; } - - /// - /// User that created the invite. - /// - public string InvitedByUserName { get; set; } - - /// - /// Email address that the invitation was sent to. - /// - public string InvitedEmailAddress { get; set; } - - /// - /// The user that accepted the invitation - /// - public string UserName { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Accounts.Events +{ + /// + /// A user have accepted an invitation. + /// + [Message] + public class InvitationAccepted + { + /// + /// Creates a new instance of . + /// + /// account that accepted the invitation + /// user that made the invite + /// userName of the person that accepted the invitation + /// invitedByUserName; userName + /// accountId + public InvitationAccepted(int accountId, string invitedByUserName, string userName) + { + if (invitedByUserName == null) throw new ArgumentNullException("invitedByUserName"); + if (userName == null) throw new ArgumentNullException("userName"); + if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); + AccountId = accountId; + InvitedByUserName = invitedByUserName; + UserName = userName; + } + + /// + /// Serialization constructor. + /// + protected InvitationAccepted() + { + } + + /// + /// The email that the invitation was accepted by. + /// + public string AcceptedEmailAddress { get; set; } + + /// + /// Id of the user that accepted the invitation + /// + public int AccountId { get; set; } + + /// + /// Applications that the user got access to. + /// + public int[] ApplicationIds { get; set; } + + /// + /// User that created the invite. + /// + public string InvitedByUserName { get; set; } + + /// + /// Email address that the invitation was sent to. + /// + public string InvitedEmailAddress { get; set; } + + /// + /// The user that accepted the invitation + /// + public string UserName { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Events/LoginFailed.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Events/LoginFailed.cs similarity index 86% rename from src/Server/OneTrueError.Api/Core/Accounts/Events/LoginFailed.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Events/LoginFailed.cs index 2deb179b..141ef93e 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Events/LoginFailed.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Events/LoginFailed.cs @@ -1,43 +1,43 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Events -{ - /// - /// A login attepmt failed - /// - public class LoginFailed : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// user that attempted to login (userName was entered by the user, it might not exist) - /// userName - public LoginFailed(string userName) - { - if (userName == null) throw new ArgumentNullException("userName"); - UserName = userName; - } - - - /// - /// If failed login was the reason (can't be set at the same time as ) - /// - public bool InvalidLogin { get; set; } - - /// - /// If account have been activated after registration. - /// - public bool IsActivated { get; set; } - - /// - /// If account was or became locked. - /// - public bool IsLocked { get; set; } - - /// - /// user that attempted to login (userName was entered by the user, it might not exist) - /// - public string UserName { get; private set; } - } +using System; + +namespace Coderr.Server.Api.Core.Accounts.Events +{ + /// + /// A login attempt failed + /// + [Message] + public class LoginFailed + { + /// + /// Creates a new instance of . + /// + /// user that attempted to login (userName was entered by the user, it might not exist) + /// userName + public LoginFailed(string userName) + { + if (userName == null) throw new ArgumentNullException("userName"); + UserName = userName; + } + + + /// + /// If failed login was the reason (can't be set at the same time as ) + /// + public bool InvalidLogin { get; set; } + + /// + /// If account have been activated after registration. + /// + public bool IsActivated { get; set; } + + /// + /// If account was or became locked. + /// + public bool IsLocked { get; set; } + + /// + /// user that attempted to login (userName was entered by the user, it might not exist) + /// + public string UserName { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Accounts/NamespaceDoc.cs new file mode 100644 index 00000000..c12f8dd4 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/NamespaceDoc.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Accounts +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// Account information (i.e. authentication and authorization) + /// + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Queries/AccountDTO.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/AccountDTO.cs similarity index 92% rename from src/Server/OneTrueError.Api/Core/Accounts/Queries/AccountDTO.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Queries/AccountDTO.cs index 060db483..f285968d 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Queries/AccountDTO.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/AccountDTO.cs @@ -1,46 +1,46 @@ -using System; - -namespace OneTrueError.Api.Core.Accounts.Queries -{ - /// - /// Account entity subset. - /// - public class AccountDTO - { - /// - /// When the account was created - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// Associated email address. - /// - //TODO: add to mapping - public string Email { get; set; } - - /// - /// Primary key - /// - public int Id { get; set; } - - /// - /// Last time user logged in. - /// - public DateTime LastLoginAtUtc { get; set; } - - /// - /// Current account state - /// - public AccountState State { get; set; } - - /// - /// When the account was updated (changed first name etc) - /// - public DateTime UpdatedAtUtc { get; set; } - - /// - /// Username - /// - public string UserName { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + /// + /// Account entity subset. + /// + public class AccountDTO + { + /// + /// When the account was created + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// Associated email address. + /// + //TODO: add to mapping + public string Email { get; set; } + + /// + /// Primary key + /// + public int Id { get; set; } + + /// + /// Last time user logged in. + /// + public DateTime LastLoginAtUtc { get; set; } + + /// + /// Current account state + /// + public AccountState State { get; set; } + + /// + /// When the account was updated (changed first name etc) + /// + public DateTime UpdatedAtUtc { get; set; } + + /// + /// Username + /// + public string UserName { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Queries/AccountStateDTO.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/AccountStateDTO.cs similarity index 89% rename from src/Server/OneTrueError.Api/Core/Accounts/Queries/AccountStateDTO.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Queries/AccountStateDTO.cs index 4381ac4a..b1912301 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Queries/AccountStateDTO.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/AccountStateDTO.cs @@ -1,28 +1,28 @@ -namespace OneTrueError.Api.Core.Accounts.Queries -{ - /// - /// Account state - /// - public enum AccountState - { - /// - /// Account have been created but not yet verified. - /// - VerificationRequired, - - /// - /// Account is active - /// - Active, - - /// - /// Account have been locked, typically by too many login attempts. - /// - Locked, - - /// - /// Password reset have been requested (an password reset link have been sent). - /// - ResetPassword - } +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + /// + /// Account state + /// + public enum AccountState + { + /// + /// Account have been created but not yet verified. + /// + VerificationRequired, + + /// + /// Account is active + /// + Active, + + /// + /// Account have been locked, typically by too many login attempts. + /// + Locked, + + /// + /// Password reset have been requested (an password reset link have been sent). + /// + ResetPassword + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Queries/FindAccountByUserName.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/FindAccountByUserName.cs similarity index 85% rename from src/Server/OneTrueError.Api/Core/Accounts/Queries/FindAccountByUserName.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Queries/FindAccountByUserName.cs index 4933d120..138de474 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Queries/FindAccountByUserName.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/FindAccountByUserName.cs @@ -1,33 +1,34 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Queries -{ - /// - /// Find an account by the given user name - /// - public class FindAccountByUserName : Query - { - /// - /// Creates a new instance of . - /// - /// username - public FindAccountByUserName(string userName) - { - if (userName == null) throw new ArgumentNullException("userName"); - UserName = userName; - } - - /// - /// Serialization constructor. - /// - protected FindAccountByUserName() - { - } - - /// - /// Username - /// - public string UserName { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + /// + /// Find an account by the given user name + /// + [Message] + public class FindAccountByUserName : Query + { + /// + /// Creates a new instance of . + /// + /// user name + public FindAccountByUserName(string userName) + { + if (userName == null) throw new ArgumentNullException("userName"); + UserName = userName; + } + + /// + /// Serialization constructor. + /// + protected FindAccountByUserName() + { + } + + /// + /// Username + /// + public string UserName { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Queries/FindAccountByUserNameResult.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/FindAccountByUserNameResult.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/Accounts/Queries/FindAccountByUserNameResult.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Queries/FindAccountByUserNameResult.cs index 5fd62711..b5c8c94a 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Queries/FindAccountByUserNameResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/FindAccountByUserNameResult.cs @@ -1,35 +1,35 @@ -using System; - -namespace OneTrueError.Api.Core.Accounts.Queries -{ - /// - /// Result for . - /// - public class FindAccountByUserNameResult - { - /// - /// Creates a new instance of . - /// - /// account id - /// Either username or FirstName LastName depending on what's available. - /// displayName - /// accountId - public FindAccountByUserNameResult(int accountId, string displayName) - { - if (displayName == null) throw new ArgumentNullException("displayName"); - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - AccountId = accountId; - DisplayName = displayName; - } - - /// - /// Account id - /// - public int AccountId { get; private set; } - - /// - /// Either username or FirstName LastName depending on what's available. - /// - public string DisplayName { get; private set; } - } +using System; + +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + /// + /// Result for . + /// + public class FindAccountByUserNameResult + { + /// + /// Creates a new instance of . + /// + /// account id + /// Either username or FirstName LastName depending on what's available. + /// displayName + /// accountId + public FindAccountByUserNameResult(int accountId, string displayName) + { + if (displayName == null) throw new ArgumentNullException("displayName"); + if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); + AccountId = accountId; + DisplayName = displayName; + } + + /// + /// Account id + /// + public int AccountId { get; private set; } + + /// + /// Either username or FirstName LastName depending on what's available. + /// + public string DisplayName { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Queries/GetAccountById.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/GetAccountById.cs similarity index 90% rename from src/Server/OneTrueError.Api/Core/Accounts/Queries/GetAccountById.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Queries/GetAccountById.cs index ae660ee1..408def10 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Queries/GetAccountById.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/GetAccountById.cs @@ -1,35 +1,36 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Queries -{ - /// - /// Get account information. - /// - public class GetAccountById : Query - { - /// - /// Creates a new instance of . - /// - /// Account id. - public GetAccountById(int accountId) - { - if (accountId < 1) - throw new ArgumentNullException("accountId"); - - AccountId = accountId; - } - - /// - /// Serialization constructor - /// - protected GetAccountById() - { - } - - /// - /// Account id. - /// - public int AccountId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + /// + /// Get account information. + /// + [Message] + public class GetAccountById : Query + { + /// + /// Creates a new instance of . + /// + /// Account id. + public GetAccountById(int accountId) + { + if (accountId < 1) + throw new ArgumentNullException("accountId"); + + AccountId = accountId; + } + + /// + /// Serialization constructor + /// + protected GetAccountById() + { + } + + /// + /// Account id. + /// + public int AccountId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Queries/GetAccountEmailById.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/GetAccountEmailById.cs similarity index 90% rename from src/Server/OneTrueError.Api/Core/Accounts/Queries/GetAccountEmailById.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Queries/GetAccountEmailById.cs index dace759d..06efd642 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Queries/GetAccountEmailById.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/GetAccountEmailById.cs @@ -1,33 +1,34 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Queries -{ - /// - /// Get email for a specific account - /// - public class GetAccountEmailById : Query - { - /// - /// Creates a new instance of . - /// - /// account - public GetAccountEmailById(int accountId) - { - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - AccountId = accountId; - } - - /// - /// Serialization constructor. - /// - protected GetAccountEmailById() - { - } - - /// - /// Account - /// - public int AccountId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + /// + /// Get email for a specific account + /// + [Message] + public class GetAccountEmailById : Query + { + /// + /// Creates a new instance of . + /// + /// account + public GetAccountEmailById(int accountId) + { + if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); + AccountId = accountId; + } + + /// + /// Serialization constructor. + /// + protected GetAccountEmailById() + { + } + + /// + /// Account + /// + public int AccountId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccounts.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccounts.cs new file mode 100644 index 00000000..540aa4f0 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccounts.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using System.Text; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + [Message] + public class ListAccounts : Query + { + } +} diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccountsResult.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccountsResult.cs new file mode 100644 index 00000000..8c347a9b --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccountsResult.cs @@ -0,0 +1,7 @@ +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + public class ListAccountsResult + { + public ListAccountsResultItem[] Accounts { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccountsResultItem.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccountsResultItem.cs new file mode 100644 index 00000000..b87b8510 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Queries/ListAccountsResultItem.cs @@ -0,0 +1,12 @@ +using System; + +namespace Coderr.Server.Api.Core.Accounts.Queries +{ + public class ListAccountsResultItem + { + public int AccountId { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public DateTime CreatedAtUtc { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Accounts/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Accounts/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Accounts/ReadMe.md diff --git a/src/Server/OneTrueError.Api/Core/Accounts/RegisterSimple.cs b/src/Server/Coderr.Server.Api/Core/Accounts/RegisterSimple.cs similarity index 88% rename from src/Server/OneTrueError.Api/Core/Accounts/RegisterSimple.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/RegisterSimple.cs index 222d5d36..fa49f513 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/RegisterSimple.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/RegisterSimple.cs @@ -1,40 +1,39 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts -{ - /// - /// Register using email address only. - /// - /// - /// - /// A temporary password is generated and included in the eamil. The user name is generated from - /// the name part of the email address. - /// - /// - public class RegisterSimple : Command - { - /// - /// Create a new instance of . - /// - /// Email address - /// emailAddress - public RegisterSimple(string emailAddress) - { - if (emailAddress == null) throw new ArgumentNullException("emailAddress"); - EmailAddress = emailAddress; - } - - /// - /// Serialization constructor. - /// - protected RegisterSimple() - { - } - - /// - /// Email address - /// - public string EmailAddress { get; private set; } - } +using System; + +namespace Coderr.Server.Api.Core.Accounts +{ + /// + /// Register using email address only. + /// + /// + /// + /// A temporary password is generated and included in the eamil. The user name is generated from + /// the name part of the email address. + /// + /// + public class RegisterSimple + { + /// + /// Create a new instance of . + /// + /// Email address + /// emailAddress + public RegisterSimple(string emailAddress) + { + if (emailAddress == null) throw new ArgumentNullException("emailAddress"); + EmailAddress = emailAddress; + } + + /// + /// Serialization constructor. + /// + protected RegisterSimple() + { + } + + /// + /// Email address + /// + public string EmailAddress { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/AcceptInvitation.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Requests/AcceptInvitation.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/Accounts/Requests/AcceptInvitation.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Requests/AcceptInvitation.cs index e05bb965..a5703232 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/AcceptInvitation.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Requests/AcceptInvitation.cs @@ -1,115 +1,115 @@ -using System; -using System.ComponentModel.DataAnnotations; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// You must create an account before accepting the invitation - /// - public class AcceptInvitation : Request - { - /// - /// Creates a new instance of . - /// - /// username - /// clear text password - /// Key from the generated email. - public AcceptInvitation(string userName, string password, string invitationKey) - { - if (userName == null) throw new ArgumentNullException("userName"); - if (password == null) throw new ArgumentNullException("password"); - if (invitationKey == null) throw new ArgumentNullException("invitationKey"); - - UserName = userName; - Password = password; - InvitationKey = invitationKey; - } - - /// - /// Creates a new instance of . - /// - /// Existing account - /// Key from the generated email. - /// - /// - /// Invite to an existing account. - /// - /// - public AcceptInvitation(int accountId, string invitationKey) - { - if (string.IsNullOrEmpty(invitationKey)) throw new ArgumentNullException("invitationKey"); - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - - AccountId = accountId; - InvitationKey = invitationKey; - } - - - /// - /// Serialization constructor - /// - protected AcceptInvitation() - { - } - - - /// - /// The email that was used when creating an account. - /// - /// - /// - /// Do note that this email can be different compared to the one that was used when sending the invitation. Make - /// sure that this one is assigned to the created account. - /// - /// - [Required] - public string AcceptedEmail { get; set; } - - /// - /// Invite to an existing account - /// - /// - /// - /// Alternative to the / combination - /// - /// - public int AccountId { get; set; } - - /// - /// Email that the inviation was sent to - /// - public string EmailUsedForTheInvitation { get; set; } - - - /// - /// First name - /// - public string FirstName { get; set; } - - /// - /// Invitation key from the invitation email. - /// - public string InvitationKey { get; private set; } - - /// - /// Last name - /// - public string LastName { get; set; } - - /// - /// Clear text password - /// - /// - public string Password { get; private set; } - - /// - /// Username as entered by the user - /// - /// - /// Used together with - /// Alternative to - /// - public string UserName { get; private set; } - } +using System; +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.Api.Core.Accounts.Requests +{ + /// + /// You must create an account before accepting the invitation + /// + [Command] + public class AcceptInvitation + { + /// + /// Creates a new instance of . + /// + /// username + /// clear text password + /// Key from the generated email. + public AcceptInvitation(string userName, string password, string invitationKey) + { + if (userName == null) throw new ArgumentNullException("userName"); + if (password == null) throw new ArgumentNullException("password"); + if (invitationKey == null) throw new ArgumentNullException("invitationKey"); + + UserName = userName; + Password = password; + InvitationKey = invitationKey; + } + + /// + /// Creates a new instance of . + /// + /// Existing account + /// Key from the generated email. + /// + /// + /// Invite to an existing account. + /// + /// + public AcceptInvitation(int accountId, string invitationKey) + { + if (string.IsNullOrEmpty(invitationKey)) throw new ArgumentNullException("invitationKey"); + if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); + + AccountId = accountId; + InvitationKey = invitationKey; + } + + + /// + /// Serialization constructor + /// + protected AcceptInvitation() + { + } + + + /// + /// The email that was used when creating an account. + /// + /// + /// + /// Do note that this email can be different compared to the one that was used when sending the invitation. Make + /// sure that this one is assigned to the created account. + /// + /// + [Required] + public string AcceptedEmail { get; set; } + + /// + /// Invite to an existing account + /// + /// + /// + /// Alternative to the / combination + /// + /// + public int AccountId { get; set; } + + /// + /// Email that the inviation was sent to + /// + public string EmailUsedForTheInvitation { get; set; } + + + /// + /// First name + /// + public string FirstName { get; set; } + + /// + /// Invitation key from the invitation email. + /// + public string InvitationKey { get; private set; } + + /// + /// Last name + /// + public string LastName { get; set; } + + /// + /// Clear text password + /// + /// + public string Password { get; private set; } + + /// + /// Username as entered by the user + /// + /// + /// Used together with + /// Alternative to + /// + public string UserName { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ChangePassword.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Requests/ChangePassword.cs similarity index 86% rename from src/Server/OneTrueError.Api/Core/Accounts/Requests/ChangePassword.cs rename to src/Server/Coderr.Server.Api/Core/Accounts/Requests/ChangePassword.cs index 630b9038..44136fe6 100644 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ChangePassword.cs +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Requests/ChangePassword.cs @@ -1,53 +1,53 @@ -using System; -using DotNetCqs; -using OneTrueError.Api.Core.Accounts.Commands; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Change password. - /// - /// - /// - /// Done when the user knows the current one but want to switch. Otherwise use . - /// - /// - public class ChangePassword : Request - { - /// - /// Create a new instance of . - /// - /// Current password - /// Password to change to. - public ChangePassword(string currentPassword, string newPassword) - { - if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); - if (newPassword == null) throw new ArgumentNullException(nameof(newPassword)); - CurrentPassword = currentPassword; - NewPassword = newPassword; - } - - /// - /// Serialization constructor. - /// - protected ChangePassword() - { - } - - /// - /// Current password - /// - public string CurrentPassword { get; private set; } - - /// - /// Password to change to. - /// - public string NewPassword { get; private set; } - - /// - /// Assigned by the CQS library - /// - [IgnoreField] - public int UserId { get; set; } - } +using System; +using Coderr.Server.Api.Core.Accounts.Commands; + +namespace Coderr.Server.Api.Core.Accounts.Requests +{ + /// + /// Change password. + /// + /// + /// + /// Done when the user knows the current one but want to switch. Otherwise use . + /// + /// + [Command] + public class ChangePassword + { + /// + /// Create a new instance of . + /// + /// Current password + /// Password to change to. + public ChangePassword(string currentPassword, string newPassword) + { + if (currentPassword == null) throw new ArgumentNullException(nameof(currentPassword)); + if (newPassword == null) throw new ArgumentNullException(nameof(newPassword)); + CurrentPassword = currentPassword; + NewPassword = newPassword; + } + + /// + /// Serialization constructor. + /// + protected ChangePassword() + { + } + + /// + /// Current password + /// + public string CurrentPassword { get; private set; } + + /// + /// Password to change to. + /// + public string NewPassword { get; private set; } + + /// + /// Assigned by the CQS library + /// + [IgnoreField] + public int UserId { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Requests/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Accounts/Requests/ReadMe.md new file mode 100644 index 00000000..fcddd636 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Requests/ReadMe.md @@ -0,0 +1,3 @@ +These are now part of the AccountService and will be moved there. + +The API will rather be HTTP services in the future, where these messages are DTOs \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Accounts/Requests/ValidateNewLoginReply.cs b/src/Server/Coderr.Server.Api/Core/Accounts/Requests/ValidateNewLoginReply.cs new file mode 100644 index 00000000..432037c7 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Accounts/Requests/ValidateNewLoginReply.cs @@ -0,0 +1,18 @@ +namespace Coderr.Server.Api.Core.Accounts.Requests +{ + /// + /// DTO + /// + public class ValidateNewLoginReply + { + /// + /// The given email address is already associated with an account. + /// + public bool EmailIsTaken { get; set; } + + /// + /// The given user name is already associated with an account. + /// + public bool UserNameIsTaken { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/CreateApiKey.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/CreateApiKey.cs new file mode 100644 index 00000000..9af258d6 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/CreateApiKey.cs @@ -0,0 +1,92 @@ +using System; + +namespace Coderr.Server.Api.Core.ApiKeys.Commands +{ + /// + /// Create a new API key + /// + /// + /// API keys are used to be able to communicate with the Coderr server through the HTTP API. + /// + [AuthorizeRoles("SysAdmin")] + [Message] + public class CreateApiKey + { + /// + /// Creates a new instance of . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// applicationName;apiKey;sharedSecret;applicationIds + public CreateApiKey(string applicationName, string apiKey, string sharedSecret, int[] applicationIds) + { + ApplicationName = applicationName ?? throw new ArgumentNullException("applicationName"); + ApiKey = apiKey ?? throw new ArgumentNullException("apiKey"); + SharedSecret = sharedSecret ?? throw new ArgumentNullException("sharedSecret"); + ApplicationIds = applicationIds ?? throw new ArgumentNullException("applicationIds"); + } + + /// + /// Creates a new instance of . + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public CreateApiKey(string applicationName, string apiKey, string sharedSecret) + { + ApplicationName = applicationName ?? throw new ArgumentNullException("applicationName"); + ApiKey = apiKey ?? throw new ArgumentNullException("apiKey"); + SharedSecret = sharedSecret ?? throw new ArgumentNullException("sharedSecret"); + ApplicationIds = new int[0]; + } + + /// + /// Serialization constructor + /// + protected CreateApiKey() + { + } + + + /// + /// Must always be the one that creates the key (will be assigned by the CommandBus per convention) + /// + public int AccountId { get; set; } + + /// + /// Generated API key + /// + public string ApiKey { get; set; } + + /// + /// applications that this key may modify. Empty = allow for all applications. + /// + public int[] ApplicationIds { get; set; } + + /// + /// Application that uses this API key + /// + public string ApplicationName { get; set; } + + /// + /// Used to sign all requests. + /// + public string SharedSecret { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Commands/DeleteApiKey.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/DeleteApiKey.cs similarity index 88% rename from src/Server/OneTrueError.Api/Core/ApiKeys/Commands/DeleteApiKey.cs rename to src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/DeleteApiKey.cs index 63225e94..f6f7ff19 100644 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Commands/DeleteApiKey.cs +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/DeleteApiKey.cs @@ -1,51 +1,52 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.ApiKeys.Commands -{ - /// - /// Delete an API key. - /// - public class DeleteApiKey : Command - { - /// - /// Serialization constructor - /// - protected DeleteApiKey() - { - } - - /// - /// Creates a new instance of . - /// - /// PK - public DeleteApiKey(int id) - { - Id = id; - } - - /// - /// Creates a new instance of . - /// - /// The generated ApiKey - public DeleteApiKey(string apiKey) - { - if (apiKey == null) throw new ArgumentNullException("apiKey"); - Guid guid; - if (!Guid.TryParse(apiKey, out guid)) - throw new ArgumentException("Not a valid api key: " + apiKey, "apiKey"); - - ApiKey = apiKey; - } - - /// - /// generated api key (if specified) - /// - public string ApiKey { get; private set; } - - /// - /// PK (if specified) - /// - public int Id { get; private set; } - } +using System; + +namespace Coderr.Server.Api.Core.ApiKeys.Commands +{ + /// + /// Delete an API key. + /// + [AuthorizeRoles("SysAdmin")] + [Message] + public class DeleteApiKey + { + /// + /// Serialization constructor + /// + protected DeleteApiKey() + { + } + + /// + /// Creates a new instance of . + /// + /// PK + public DeleteApiKey(int id) + { + Id = id; + } + + /// + /// Creates a new instance of . + /// + /// The generated ApiKey + public DeleteApiKey(string apiKey) + { + if (apiKey == null) throw new ArgumentNullException("apiKey"); + Guid guid; + if (!Guid.TryParse(apiKey, out guid)) + throw new ArgumentException("Not a valid api key: " + apiKey, "apiKey"); + + ApiKey = apiKey; + } + + /// + /// generated api key (if specified) + /// + public string ApiKey { get; private set; } + + /// + /// PK (if specified) + /// + public int Id { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/EditApiKey.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/EditApiKey.cs new file mode 100644 index 00000000..71e71361 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Commands/EditApiKey.cs @@ -0,0 +1,47 @@ +using System; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + +namespace Coderr.Server.Api.Core.ApiKeys.Commands +{ + /// + /// Create a new api key + /// + [AuthorizeRoles("SysAdmin")] + [Message] + public class EditApiKey + { + /// + /// Creates a new instance of . + /// + public EditApiKey(int id) + { + if (id <= 0) throw new ArgumentOutOfRangeException(nameof(id)); + Id = id; + } + + + /// + /// Serialization constructor + /// + protected EditApiKey() + { + } + + + /// + /// applications that this key may modify. Empty = allow for all applications. + /// + public int[] ApplicationIds { get; set; } + + /// + /// Application that uses this api key + /// + public string ApplicationName { get; set; } + + /// + /// Key id + /// + public int Id { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Events/ApiKeyCreated.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Events/ApiKeyCreated.cs similarity index 95% rename from src/Server/OneTrueError.Api/Core/ApiKeys/Events/ApiKeyCreated.cs rename to src/Server/Coderr.Server.Api/Core/ApiKeys/Events/ApiKeyCreated.cs index 4ee5bf1f..e0801678 100644 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Events/ApiKeyCreated.cs +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Events/ApiKeyCreated.cs @@ -1,12 +1,12 @@ using System; -using DotNetCqs; -namespace OneTrueError.Api.Core.ApiKeys.Events +namespace Coderr.Server.Api.Core.ApiKeys.Events { - /// - /// A new API key has been created. + /// + /// A new API key has been created. /// - public class ApiKeyCreated : ApplicationEvent + [Message] + public class ApiKeyCreated { /// /// Creates a new instance of . diff --git a/src/Server/Coderr.Server.Api/Core/ApiKeys/Events/ApiKeyRemoved.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Events/ApiKeyRemoved.cs new file mode 100644 index 00000000..644fa18e --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Events/ApiKeyRemoved.cs @@ -0,0 +1,10 @@ +namespace Coderr.Server.Api.Core.ApiKeys.Events +{ + /// + /// A API key was removed from the system + /// + [Event] + public class ApiKeyRemoved + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKey.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKey.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKey.cs rename to src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKey.cs index 5c8c0d8d..d4447ff3 100644 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKey.cs +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKey.cs @@ -1,51 +1,52 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.ApiKeys.Queries -{ - /// - /// Get information about an API key - /// - public class GetApiKey : Query - { - /// - /// Serialization constructor - /// - protected GetApiKey() - { - } - - /// - /// Creates a new instance of . - /// - /// PK - public GetApiKey(int id) - { - Id = id; - } - - /// - /// Creates a new instance of . - /// - /// The generated ApiKey - public GetApiKey(string apiKey) - { - if (apiKey == null) throw new ArgumentNullException("apiKey"); - Guid guid; - if (!Guid.TryParse(apiKey, out guid)) - throw new ArgumentException("Not a valid api key: " + apiKey, "apiKey"); - - ApiKey = apiKey; - } - - /// - /// generated api key (if specified) - /// - public string ApiKey { get; private set; } - - /// - /// PK (if specified) - /// - public int Id { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.ApiKeys.Queries +{ + /// + /// Get information about an API key + /// + [Message] + public class GetApiKey : Query + { + /// + /// Serialization constructor + /// + protected GetApiKey() + { + } + + /// + /// Creates a new instance of . + /// + /// PK + public GetApiKey(int id) + { + Id = id; + } + + /// + /// Creates a new instance of . + /// + /// The generated ApiKey + public GetApiKey(string apiKey) + { + if (apiKey == null) throw new ArgumentNullException("apiKey"); + Guid guid; + if (!Guid.TryParse(apiKey, out guid)) + throw new ArgumentException("Not a valid api key: " + apiKey, "apiKey"); + + ApiKey = apiKey; + } + + /// + /// generated api key (if specified) + /// + public string ApiKey { get; private set; } + + /// + /// PK (if specified) + /// + public int Id { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKeyResult.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKeyResult.cs similarity index 92% rename from src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKeyResult.cs rename to src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKeyResult.cs index 9bbdd784..869f9a33 100644 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKeyResult.cs +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKeyResult.cs @@ -1,48 +1,48 @@ -using System; - -namespace OneTrueError.Api.Core.ApiKeys.Queries -{ - /// - /// Result for . - /// - public class GetApiKeyResult - { - /// - /// Application ids that we've been granted to work with - /// - public GetApiKeyResultApplication[] AllowedApplications { get; set; } - - /// - /// Application that will be using this key - /// - public string ApplicationName { get; set; } - - - /// - /// When this key was generated - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// AccountId that generated this key - /// - public int CreatedById { get; set; } - - /// - /// Api key - /// - public string GeneratedKey { get; set; } - - - /// - /// PK - /// - public int Id { get; set; } - - - /// - /// Used when generating signatures. - /// - public string SharedSecret { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.ApiKeys.Queries +{ + /// + /// Result for . + /// + public class GetApiKeyResult + { + /// + /// Application ids that we've been granted to work with + /// + public GetApiKeyResultApplication[] AllowedApplications { get; set; } + + /// + /// Application that will be using this key + /// + public string ApplicationName { get; set; } + + + /// + /// When this key was generated + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// AccountId that generated this key + /// + public int CreatedById { get; set; } + + /// + /// Api key + /// + public string GeneratedKey { get; set; } + + + /// + /// PK + /// + public int Id { get; set; } + + + /// + /// Used when generating signatures. + /// + public string SharedSecret { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKeyResultApplication.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKeyResultApplication.cs similarity index 86% rename from src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKeyResultApplication.cs rename to src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKeyResultApplication.cs index 7528845b..fca1862f 100644 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/GetApiKeyResultApplication.cs +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/GetApiKeyResultApplication.cs @@ -1,19 +1,19 @@ -namespace OneTrueError.Api.Core.ApiKeys.Queries -{ - /// - /// An allowed application for . - /// - public class GetApiKeyResultApplication - { - /// - /// Application id (PK) - /// - public int ApplicationId { get; set; } - - - /// - /// Name of the application - /// - public string ApplicationName { get; set; } - } +namespace Coderr.Server.Api.Core.ApiKeys.Queries +{ + /// + /// An allowed application for . + /// + public class GetApiKeyResultApplication + { + /// + /// Application id (PK) + /// + public int ApplicationId { get; set; } + + + /// + /// Name of the application + /// + public string ApplicationName { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeys.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeys.cs new file mode 100644 index 00000000..bd217a17 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeys.cs @@ -0,0 +1,12 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Core.ApiKeys.Queries +{ + /// + /// List all created keys + /// + [Message] + public class ListApiKeys : Query + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeysResult.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeysResult.cs similarity index 81% rename from src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeysResult.cs rename to src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeysResult.cs index b5896464..61bd3890 100644 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeysResult.cs +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeysResult.cs @@ -1,13 +1,13 @@ -namespace OneTrueError.Api.Core.ApiKeys.Queries -{ - /// - /// Result for . - /// - public class ListApiKeysResult - { - /// - /// All created keys - /// - public ListApiKeysResultItem[] Keys { get; set; } - } +namespace Coderr.Server.Api.Core.ApiKeys.Queries +{ + /// + /// Result for . + /// + public class ListApiKeysResult + { + /// + /// All created keys + /// + public ListApiKeysResultItem[] Keys { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeysResultItem.cs b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeysResultItem.cs similarity index 88% rename from src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeysResultItem.cs rename to src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeysResultItem.cs index 018b9a0f..20bfc7bf 100644 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeysResultItem.cs +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/Queries/ListApiKeysResultItem.cs @@ -1,23 +1,23 @@ -namespace OneTrueError.Api.Core.ApiKeys.Queries -{ - /// - /// Item for . - /// - public class ListApiKeysResultItem - { - /// - /// Key to use - /// - public string ApiKey { get; set; } - - /// - /// Application name, i.e. name of the application that uses this key. - /// - public string ApplicationName { get; set; } - - /// - /// Identity - /// - public int Id { get; set; } - } +namespace Coderr.Server.Api.Core.ApiKeys.Queries +{ + /// + /// Item for . + /// + public class ListApiKeysResultItem + { + /// + /// Key to use + /// + public string ApiKey { get; set; } + + /// + /// Application name, i.e. name of the application that uses this key. + /// + public string ApplicationName { get; set; } + + /// + /// Identity + /// + public int Id { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/ApiKeys/ReadMe.md b/src/Server/Coderr.Server.Api/Core/ApiKeys/ReadMe.md new file mode 100644 index 00000000..92f887cc --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/ApiKeys/ReadMe.md @@ -0,0 +1,32 @@ +ApiKeys +======== + +Used to allow external applications to talk with codeRR. + + +## Example usage + +The following example calls a local codeRR server to retreive applications. + +```csharp +var client = new ServerApiClient(); +var uri = new Uri("http://yourServer/coderr/"); +client.Open(uri, "theApiKey", "sharedSecret"); +var apps = await client.QueryAsync(new GetApplicationList()); +``` + +Result (serialized as JSON): + +```javascript +[{ + "Id" : 1, + "Name" : "PublicWeb" + }, { + "Id" : 9, + "Name" : "Time reporting system" + }, { + "Id" : 10, + "Name" : "Coffee monitor" + } +] +``` diff --git a/src/Server/Coderr.Server.Api/Core/Applications/ApplicationListItem.cs b/src/Server/Coderr.Server.Api/Core/Applications/ApplicationListItem.cs new file mode 100644 index 00000000..e9d86f7a --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/ApplicationListItem.cs @@ -0,0 +1,64 @@ +using System; +using Coderr.Server.Api.Core.Applications.Queries; + +namespace Coderr.Server.Api.Core.Applications +{ + /// + /// Result item for + /// + public class ApplicationListItem + { + /// + /// Creates a new instance of . + /// + /// application identity + /// name of the application + public ApplicationListItem(int id, string name) + { + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + + Id = id; + Name = name ?? throw new ArgumentNullException("name"); + } + + /// + /// Serialization constructor + /// + protected ApplicationListItem() + { + } + + /// + /// Group that this application belongs to. + /// + public int GroupId { get; set; } + + /// + /// Name of the group + /// + public string GroupName { get; set; } + + /// + /// Id of the application (primary key) + /// + public int Id { get; set; } + + /// + /// User that requested this list is the admin of the specified application. + /// + public bool IsAdmin { get; set; } + + /// + /// Application name as entered by the user. + /// + public string Name { get; set; } + + /// + /// Number of full time developers. + /// + /// + /// null = not specified + /// + public decimal? NumberOfDevelopers { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/AddTeamMember.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/AddTeamMember.cs new file mode 100644 index 00000000..5294c573 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/AddTeamMember.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Coderr.Server.Api.Core.Applications.Commands +{ + [Command] + public class AddTeamMember + { + public int UserToAdd { get; set; } + public int ApplicationId { get; set; } + + public string[] Roles { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/CreateApplication.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/CreateApplication.cs new file mode 100644 index 00000000..87f7d151 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/CreateApplication.cs @@ -0,0 +1,69 @@ +using System; + +namespace Coderr.Server.Api.Core.Applications.Commands +{ + /// + /// Create a new application. + /// + [Message] + public class CreateApplication + { + /// + /// Creates a new instance of . + /// + /// Name of the application (as entered by the user) + /// Application type + public CreateApplication(string name, TypeOfApplication typeOfApplication) + { + if (name == null) throw new ArgumentNullException("name"); + if (!Enum.IsDefined(typeof(TypeOfApplication), typeOfApplication)) + + throw new ArgumentOutOfRangeException("typeOfApplication"); + Name = name; + TypeOfApplication = typeOfApplication; + } + + /// + /// Generated application key + /// + public string ApplicationKey { get; set; } + + /// + /// User specified name + /// + public string Name { get; set; } + + + /// + /// Application type + /// + public TypeOfApplication TypeOfApplication { get; set; } + + + /// + /// Account id for the user that sent the command + /// + [IgnoreField] + public int UserId { get; set; } + + /// + /// Estimated number of errors + /// + public int? NumberOfErrors { get; set; } + + /// + /// Number of developers that work full time with this application. + /// + public decimal? NumberOfDevelopers { get; set; } + + /// + /// Number of days to keep new incidents. + /// + public int? RetentionDays { get; set; } + + /// + /// Application group that this application should be part of. + /// + public int? GroupId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/CreateApplicationGroup.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/CreateApplicationGroup.cs new file mode 100644 index 00000000..c3375a84 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/CreateApplicationGroup.cs @@ -0,0 +1,37 @@ +using System; + +namespace Coderr.Server.Api.Core.Applications.Commands +{ + /// + /// Create an application group. + /// + /// + /// + /// Groups are used to group similar applications together. + /// + /// + [Command] + public class CreateApplicationGroup + { + /// + /// Creates a new instance of the class. + /// + /// Name of the new group (human friendly name) + public CreateApplicationGroup(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Serialization constructor + /// + private CreateApplicationGroup() + { + } + + /// + /// Name of the new group (human friendly name). + /// + public string Name { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Commands/DeleteApplication.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/DeleteApplication.cs similarity index 84% rename from src/Server/OneTrueError.Api/Core/Applications/Commands/DeleteApplication.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Commands/DeleteApplication.cs index 8c501e11..c6f391d2 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Commands/DeleteApplication.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/DeleteApplication.cs @@ -1,12 +1,12 @@ using System; -using DotNetCqs; -namespace OneTrueError.Api.Core.Applications.Commands +namespace Coderr.Server.Api.Core.Applications.Commands { /// /// Delete an existing application including of all its data. /// - public class DeleteApplication : Command + [Message] + public class DeleteApplication { /// /// Creates a new instance of . diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/DeleteApplicationGroup.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/DeleteApplicationGroup.cs new file mode 100644 index 00000000..40029da2 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/DeleteApplicationGroup.cs @@ -0,0 +1,43 @@ +namespace Coderr.Server.Api.Core.Applications.Commands +{ + /// + /// Create an application group. + /// + /// + /// + /// Groups are used to group similar applications together. + /// + /// + [Command] + public class DeleteApplicationGroup + { + /// + /// Creates a new instance of the class. + /// + /// Group to delete + /// Move all applications to this group + public DeleteApplicationGroup(int groupId, int moveAppsToGroupId) + { + GroupId = groupId; + MoveAppsToGroupId = moveAppsToGroupId; + } + + /// + /// Serialization constructor + /// + private DeleteApplicationGroup() + { + } + + /// + /// Group to delete + /// + public int GroupId { get; private set; } + + /// + /// Move all applications to this group + /// + public int MoveAppsToGroupId { get; private set; } + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/MapApplicationsToGroup.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/MapApplicationsToGroup.cs new file mode 100644 index 00000000..0b4e660a --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/MapApplicationsToGroup.cs @@ -0,0 +1,31 @@ +using System; + +namespace Coderr.Server.Api.Core.Applications.Commands +{ + [Message] + public class MapApplicationsToGroup + { + public MapApplicationsToGroup(int groupId, int[] applicationIds) + { + if (applicationIds == null) throw new ArgumentNullException(nameof(applicationIds)); + if (groupId <= 0) throw new ArgumentOutOfRangeException(nameof(groupId)); + GroupId = groupId; + ApplicationIds = applicationIds ?? throw new ArgumentNullException(nameof(applicationIds)); + } + + protected MapApplicationsToGroup() + { + + } + + /// + /// Applications to assign to the group. + /// + public int[] ApplicationIds { get; private set; } + + /// + /// Group that the applications should be assigned to. + /// + public int GroupId { get; private set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/MuteStatisticsQuestion.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/MuteStatisticsQuestion.cs new file mode 100644 index 00000000..967f1433 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/MuteStatisticsQuestion.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Coderr.Server.Api.Core.Applications.Commands +{ + /// + /// Do not pester the user with the question about adding info for statistics. + /// + [Command] + public class MuteStatisticsQuestion + { + public int ApplicationId { get; set; } + } +} diff --git a/src/Server/OneTrueError.Api/Core/Applications/Commands/RemoveTeamMember.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/RemoveTeamMember.cs similarity index 89% rename from src/Server/OneTrueError.Api/Core/Applications/Commands/RemoveTeamMember.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Commands/RemoveTeamMember.cs index 5a649553..e02fa687 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Commands/RemoveTeamMember.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/RemoveTeamMember.cs @@ -1,12 +1,12 @@ using System; -using DotNetCqs; -namespace OneTrueError.Api.Core.Applications.Commands +namespace Coderr.Server.Api.Core.Applications.Commands { /// /// Remove a team member from the /// - public class RemoveTeamMember : Command + [Message] + public class RemoveTeamMember { /// /// Creates a new instance of . diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/RenameApplicationGroup.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/RenameApplicationGroup.cs new file mode 100644 index 00000000..0f52dbad --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/RenameApplicationGroup.cs @@ -0,0 +1,18 @@ +using System; + +namespace Coderr.Server.Api.Core.Applications.Commands +{ + [Message] + public class RenameApplicationGroup + { + public RenameApplicationGroup(int groupId, string newName) + { + if (groupId <= 0) throw new ArgumentOutOfRangeException(nameof(groupId)); + GroupId = groupId; + NewName = newName ?? throw new ArgumentNullException(nameof(newName)); + } + + public int GroupId { get; private set; } + public string NewName { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/SetApplicationGroup.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/SetApplicationGroup.cs new file mode 100644 index 00000000..be3532f6 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/SetApplicationGroup.cs @@ -0,0 +1,56 @@ +namespace Coderr.Server.Api.Core.Applications.Commands +{ + /// + /// Assign an application to a group. + /// + /// + /// + /// Groups are used to make it easier to navigate between applications. + /// + /// + [Command] + public class SetApplicationGroup + { + /// + /// Creates a new instance of the class. + /// + /// Application to assign a group to. + /// Group to assign the application to + public SetApplicationGroup(int applicationId, int applicationGroupId) + { + ApplicationId = applicationId; + ApplicationGroupId = applicationGroupId; + } + + /// + /// Creates a new instance of the class. + /// + /// Application to assign a group to. + /// Group to assign the application to + public SetApplicationGroup(int applicationId, string groupName) + { + ApplicationId = applicationId; + GroupName = groupName; + } + + protected SetApplicationGroup() + { + + } + + /// + /// Group to show the application under. + /// + public int ApplicationGroupId { get; private set; } + + /// + /// Application which should appear under the group + /// + public int ApplicationId { get; private set; } + + /// + /// Group name (you can specify either the group Id or the group name) + /// + public string GroupName { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Commands/UpdateApplication.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/UpdateApplication.cs similarity index 83% rename from src/Server/OneTrueError.Api/Core/Applications/Commands/UpdateApplication.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Commands/UpdateApplication.cs index 2c7043d2..4002c5cc 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Commands/UpdateApplication.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/UpdateApplication.cs @@ -1,12 +1,12 @@ using System; -using DotNetCqs; -namespace OneTrueError.Api.Core.Applications.Commands +namespace Coderr.Server.Api.Core.Applications.Commands { /// /// Update application /// - public class UpdateApplication : Command + [Message] + public class UpdateApplication { /// /// Creates a new instance of . @@ -40,5 +40,10 @@ public UpdateApplication(int applicationId, string name) /// /// public TypeOfApplication? TypeOfApplication { get; set; } + + /// + /// Number of days to keep new incidents. + /// + public int? RetentionDays { get; set; } } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Commands/UpdateRoles.cs b/src/Server/Coderr.Server.Api/Core/Applications/Commands/UpdateRoles.cs new file mode 100644 index 00000000..ea86dceb --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Commands/UpdateRoles.cs @@ -0,0 +1,11 @@ +namespace Coderr.Server.Api.Core.Applications.Commands +{ + [Command] + public class UpdateRoles + { + public int UserToUpdate { get; set; } + public int ApplicationId { get; set; } + + public string[] Roles { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Events/ApplicationCreated.cs b/src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationCreated.cs similarity index 92% rename from src/Server/OneTrueError.Api/Core/Applications/Events/ApplicationCreated.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationCreated.cs index d900a9d7..fa4ba596 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Events/ApplicationCreated.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationCreated.cs @@ -1,67 +1,69 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Events -{ - /// - /// Published when a new application have been created by a user. - /// - public class ApplicationCreated : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// application identity - /// name as specified by the user - /// account id for the user that created the application - /// appKey used to identify the application during uploads. - /// Used with to authenticate the upload. - public ApplicationCreated(int id, string name, int createdById, string appKey, string sharedSecret) - { - if (name == null) throw new ArgumentNullException("name"); - if (appKey == null) throw new ArgumentNullException("appKey"); - if (sharedSecret == null) throw new ArgumentNullException("sharedSecret"); - if (id <= 0) throw new ArgumentOutOfRangeException("id"); - if (createdById <= 0) throw new ArgumentOutOfRangeException("createdById"); - - CreatedById = createdById; - AppKey = appKey; - SharedSecret = sharedSecret; - ApplicationId = id; - ApplicationName = name; - } - - /// - /// Serialization constructor - /// - protected ApplicationCreated() - { - } - - /// - /// Application key which is used to identify the application that uploads a report. - /// - public string AppKey { get; set; } - - - /// - /// Application identity - /// - public int ApplicationId { get; set; } - - /// - /// Name as entered by the user. - /// - public string ApplicationName { get; private set; } - - /// - /// Account id of the person that created this application - /// - public int CreatedById { get; private set; } - - /// - /// Used together with the to be able to authenticate the upload. - /// - public string SharedSecret { get; set; } - } +using System; + +// ReSharper disable All + +namespace Coderr.Server.Api.Core.Applications.Events +{ + /// + /// Published when a new application have been created by a user. + /// + [Message] + public class ApplicationCreated + { + /// + /// Creates a new instance of . + /// + /// application identity + /// name as specified by the user + /// account id for the user that created the application + /// appKey used to identify the application during uploads. + /// Used with to authenticate the upload. + public ApplicationCreated(int id, string name, int createdById, string appKey, string sharedSecret) + { + if (name == null) throw new ArgumentNullException("name"); + if (appKey == null) throw new ArgumentNullException("appKey"); + if (sharedSecret == null) throw new ArgumentNullException("sharedSecret"); + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + if (createdById <= 0) throw new ArgumentOutOfRangeException("createdById"); + + CreatedById = createdById; + AppKey = appKey; + SharedSecret = sharedSecret; + ApplicationId = id; + ApplicationName = name; + } + + /// + /// Serialization constructor + /// + protected ApplicationCreated() + { + } + + /// + /// Application key which is used to identify the application that uploads a report. + /// + public string AppKey { get; set; } + + + /// + /// Application identity + /// + public int ApplicationId { get; set; } + + /// + /// Name as entered by the user. + /// + public string ApplicationName { get; private set; } + + /// + /// Account id of the person that created this application + /// + public int CreatedById { get; private set; } + + /// + /// Used together with the to be able to authenticate the upload. + /// + public string SharedSecret { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Events/ApplicationDeleted.cs b/src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationDeleted.cs similarity index 79% rename from src/Server/OneTrueError.Api/Core/Applications/Events/ApplicationDeleted.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationDeleted.cs index 90012346..b9fbfdc2 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Events/ApplicationDeleted.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationDeleted.cs @@ -1,11 +1,10 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Events +namespace Coderr.Server.Api.Core.Applications.Events { /// /// An application have been deleted. /// - public class ApplicationDeleted : ApplicationEvent + [Message] + public class ApplicationDeleted { /// /// Key used when uploading reports diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationGroupCreated.cs b/src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationGroupCreated.cs new file mode 100644 index 00000000..e45acae0 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Events/ApplicationGroupCreated.cs @@ -0,0 +1,21 @@ +namespace Coderr.Server.Api.Core.Applications.Events +{ + public class ApplicationGroupCreated + { + public ApplicationGroupCreated(int id, string name, int createdById) + { + Id = id; + Name = name; + CreatedById = createdById; + } + + protected ApplicationGroupCreated() + { + + } + + public int CreatedById { get; private set; } + public int Id { get; private set; } + public string Name { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Events/UserAddedToApplication.cs b/src/Server/Coderr.Server.Api/Core/Applications/Events/UserAddedToApplication.cs new file mode 100644 index 00000000..e45a15d4 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Events/UserAddedToApplication.cs @@ -0,0 +1,37 @@ +namespace Coderr.Server.Api.Core.Applications.Events +{ + /// + /// A user have been added directly, or through an invitation + /// + [Message] + public class UserAddedToApplication + { + /// + /// Creates a new instance of . + /// + /// Identifier for the application that the user was added to. + /// Account identifier for the user that was added to the application + public UserAddedToApplication(int applicationId, int accountId) + { + ApplicationId = applicationId; + AccountId = accountId; + } + + /// + /// Serialization constructor + /// + protected UserAddedToApplication() + { + } + + /// + /// Account identifier for the user that was added to the application + /// + public int AccountId { get; private set; } + + /// + /// Identifier for the application that the user was added to. + /// + public int ApplicationId { get; private set; } + } +} diff --git a/src/Server/OneTrueError.Api/Core/Applications/Events/UserInvitedToApplication.cs b/src/Server/Coderr.Server.Api/Core/Applications/Events/UserInvitedToApplication.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Applications/Events/UserInvitedToApplication.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Events/UserInvitedToApplication.cs index 555e3c1e..ea6b2cd3 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Events/UserInvitedToApplication.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Events/UserInvitedToApplication.cs @@ -1,68 +1,68 @@ -using System; -using DotNetCqs; -using OneTrueError.Api.Core.Invitations.Commands; - -namespace OneTrueError.Api.Core.Applications.Events -{ - /// - /// Event published when the command is done. - /// - public class UserInvitedToApplication : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// Key that the user clicks on in the invitation email - /// Application that the user was invited to - /// application name - /// Email address that the invitation was sent to - /// Username for the user that made the invitation - /// emailAddress; invitedBy - /// applicationId - public UserInvitedToApplication(string invitationKey, int applicationId, string applicationName, - string emailAddress, string invitedBy) - { - if (invitationKey == null) throw new ArgumentNullException("invitationKey"); - if (emailAddress == null) throw new ArgumentNullException("emailAddress"); - if (invitedBy == null) throw new ArgumentNullException("invitedBy"); - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - InvitationKey = invitationKey; - ApplicationId = applicationId; - ApplicationName = applicationName; - EmailAddress = emailAddress; - InvitedBy = invitedBy; - } - - /// - /// Serialization constructor - /// - protected UserInvitedToApplication() - { - } - - /// - /// Application that the user will gain access to. - /// - public int ApplicationId { get; private set; } - - /// - /// Application name - /// - public string ApplicationName { get; private set; } - - /// - /// Email address to the invited user. - /// - public string EmailAddress { get; private set; } - - /// - /// Identifier sent in the invitation email. - /// - public string InvitationKey { get; private set; } - - /// - /// Username of the user that invited the other user. - /// - public string InvitedBy { get; private set; } - } +using System; +using Coderr.Server.Api.Core.Invitations.Commands; + +namespace Coderr.Server.Api.Core.Applications.Events +{ + /// + /// Event published when the command is done. + /// + [Message] + public class UserInvitedToApplication + { + /// + /// Creates a new instance of . + /// + /// Key that the user clicks on in the invitation email + /// Application that the user was invited to + /// application name + /// Email address that the invitation was sent to + /// Username for the user that made the invitation + /// emailAddress; invitedBy + /// applicationId + public UserInvitedToApplication(string invitationKey, int applicationId, string applicationName, + string emailAddress, string invitedBy) + { + if (invitationKey == null) throw new ArgumentNullException("invitationKey"); + if (emailAddress == null) throw new ArgumentNullException("emailAddress"); + if (invitedBy == null) throw new ArgumentNullException("invitedBy"); + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + InvitationKey = invitationKey; + ApplicationId = applicationId; + ApplicationName = applicationName; + EmailAddress = emailAddress; + InvitedBy = invitedBy; + } + + /// + /// Serialization constructor + /// + protected UserInvitedToApplication() + { + } + + /// + /// Application that the user will gain access to. + /// + public int ApplicationId { get; private set; } + + /// + /// Application name + /// + public string ApplicationName { get; private set; } + + /// + /// Email address to the invited user. + /// + public string EmailAddress { get; private set; } + + /// + /// Identifier sent in the invitation email. + /// + public string InvitationKey { get; private set; } + + /// + /// Username of the user that invited the other user. + /// + public string InvitedBy { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Applications/NamespaceDoc.cs new file mode 100644 index 00000000..0c3dd6b3 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/NamespaceDoc.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Applications +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// An application that we can receive exceptions for. + /// + /// + /// + /// It can also be a specific tier in an application. For instance ASP.NET WebApi, or client side (like an Mobile + /// application). + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMap.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMap.cs new file mode 100644 index 00000000..dc832b03 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMap.cs @@ -0,0 +1,10 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + [Message] + public class GetApplicationGroupMap : Query + { + public int? ApplicationId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMapResult.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMapResult.cs new file mode 100644 index 00000000..471a66ee --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMapResult.cs @@ -0,0 +1,10 @@ +namespace Coderr.Server.Api.Core.Applications.Queries +{ + public class GetApplicationGroupMapResult + { + /// + /// If application id was specified, this will only contain one item; otherwise all maps. + /// + public GetApplicationGroupMapResultItem[] Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMapResultItem.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMapResultItem.cs new file mode 100644 index 00000000..bb03f499 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupMapResultItem.cs @@ -0,0 +1,8 @@ +namespace Coderr.Server.Api.Core.Applications.Queries +{ + public class GetApplicationGroupMapResultItem + { + public int ApplicationId { get; set; } + public int GroupId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroups.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroups.cs new file mode 100644 index 00000000..9ad4bfbb --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroups.cs @@ -0,0 +1,9 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + [Message] + public class GetApplicationGroups : Query + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupsResult.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupsResult.cs new file mode 100644 index 00000000..ea64c085 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupsResult.cs @@ -0,0 +1,7 @@ +namespace Coderr.Server.Api.Core.Applications.Queries +{ + public class GetApplicationGroupsResult + { + public GetApplicationGroupsResultItem[] Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupsResultItem.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupsResultItem.cs new file mode 100644 index 00000000..622fa70f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationGroupsResultItem.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.Api.Core.Applications.Queries +{ + public class GetApplicationGroupsResultItem + { + public int Id { get; set; } + public string Name { get; set; } + public int[] ApplicationIds { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationIdByKey.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationIdByKey.cs similarity index 92% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationIdByKey.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationIdByKey.cs index 0f850351..f24ce9c7 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationIdByKey.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationIdByKey.cs @@ -1,36 +1,37 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Get an application by using the AppKey. - /// - public class GetApplicationIdByKey : Query - { - /// - /// Creates a new instance of . - /// - /// appKey (GUID) - public GetApplicationIdByKey(string applicationKey) - { - if (applicationKey == null) throw new ArgumentNullException("applicationKey"); - Guid uid; - if (!Guid.TryParse(applicationKey, out uid)) - throw new FormatException("'" + applicationKey + "' is not a valid application key."); - ApplicationKey = applicationKey; - } - - /// - /// Serialization constructor - /// - protected GetApplicationIdByKey() - { - } - - /// - /// AppKey - /// - public string ApplicationKey { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Get an application by using the AppKey. + /// + [Message] + public class GetApplicationIdByKey : Query + { + /// + /// Creates a new instance of . + /// + /// appKey (GUID) + public GetApplicationIdByKey(string applicationKey) + { + if (applicationKey == null) throw new ArgumentNullException("applicationKey"); + Guid uid; + if (!Guid.TryParse(applicationKey, out uid)) + throw new FormatException("'" + applicationKey + "' is not a valid application key."); + ApplicationKey = applicationKey; + } + + /// + /// Serialization constructor + /// + protected GetApplicationIdByKey() + { + } + + /// + /// AppKey + /// + public string ApplicationKey { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationIdByKeyResult.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationIdByKeyResult.cs similarity index 80% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationIdByKeyResult.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationIdByKeyResult.cs index 53421366..6e57336e 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationIdByKeyResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationIdByKeyResult.cs @@ -1,13 +1,13 @@ -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Result for . - /// - public class GetApplicationIdByKeyResult - { - /// - /// Application id - /// - public int Id { get; set; } - } +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Result for . + /// + public class GetApplicationIdByKeyResult + { + /// + /// Application id + /// + public int Id { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationInfo.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationInfo.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationInfo.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationInfo.cs index 6e2c4a1a..03f39724 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationInfo.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationInfo.cs @@ -1,73 +1,75 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Get information for an application either by using the key or application id - /// - public class GetApplicationInfo : Query - { - private string _appKey; - private int _applicationId; - - /// - /// Creates a new instance of . - /// - /// identity of the application - public GetApplicationInfo(int id) - { - if (id <= 0) throw new ArgumentOutOfRangeException("id"); - ApplicationId = id; - } - - /// - /// Creates a new instance of . - /// - /// Application key used when sending error reports - public GetApplicationInfo(string appKey) - { - if (appKey == null) throw new ArgumentNullException("appKey"); - AppKey = appKey; - } - - /// - /// Creates a new instance of . - /// - protected GetApplicationInfo() - { - } - - /// - /// Application key from the user interface - /// - /// Not a valid application key. - public string AppKey - { - get { return _appKey; } - set - { - Guid uid; - if (!Guid.TryParse(value, out uid)) - throw new FormatException("'" + value + "' is not a valid application key."); - - _appKey = value; - } - } - - /// - /// Application id - /// - /// Not a valid application id - public int ApplicationId - { - get { return _applicationId; } - set - { - if (value <= 0) - throw new ArgumentOutOfRangeException("value", value, "Not a valid id."); - _applicationId = value; - } - } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Get information for an application either by using the key or application id + /// + [Message] + public class GetApplicationInfo : Query + { + private string _appKey; + private int _applicationId; + + /// + /// Creates a new instance of . + /// + /// identity of the application + public GetApplicationInfo(int id) + { + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + ApplicationId = id; + } + + /// + /// Creates a new instance of . + /// + /// Application key used when sending error reports + public GetApplicationInfo(string appKey) + { + if (appKey == null) throw new ArgumentNullException("appKey"); + AppKey = appKey; + } + + /// + /// Creates a new instance of . + /// + protected GetApplicationInfo() + { + } + + /// + /// Application key from the user interface + /// + /// Not a valid application key. + public string AppKey + { + get { return _appKey; } + set + { + Guid uid; + if (!Guid.TryParse(value, out uid)) + throw new FormatException("'" + value + "' is not a valid application key."); + + _appKey = value; + } + } + + /// + /// Application id + /// + /// Not a valid application id + public int ApplicationId + { + get { return _applicationId; } + set + { + // Will be 0 when appKey is specified. + if (value < 0) + throw new ArgumentOutOfRangeException("value", value, "Not a valid id."); + _applicationId = value; + } + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationInfoResult.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationInfoResult.cs new file mode 100644 index 00000000..996c842c --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationInfoResult.cs @@ -0,0 +1,75 @@ +using System; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Result for . + /// + public class GetApplicationInfoResult + { + /// + /// Application key + /// + public string AppKey { get; set; } + + /// + /// Type of application + /// + public TypeOfApplication ApplicationType { get; set; } + + /// + /// Application id + /// + public int Id { get; set; } + + /// + /// Name of the application. + /// + public string Name { get; set; } + + /// + /// Shared secret, used together with to make sure that the reports come from the correct source. + /// + public string SharedSecret { get; set; } + + /// + /// Total number of incidents for this application. + /// + public int TotalIncidentCount { get; set; } + + /// + /// Number of full time developers working on this application (1.5 = one full time and one half time) + /// + /// + /// + ///null = not specified + /// + /// + public decimal? NumberOfDevelopers { get; set; } + + /// + /// Versions that we have received error reports for. + /// + public string[] Versions { get; set; } + + /// + /// Got information to be able to compare how the team is performing with other teams. + /// + public bool ShowStatsQuestion { get; set; } + + /// + /// Number of days to keep new incidents before deleting them. + /// + /// + /// + /// We've seen that errors that aren't within 60 days aren't fixed at all. It's therefore better to delete them than to keep them in the system (and adding noise to the error list). + /// + /// + public int RetentionDays { get; set; } + + /// + /// When we received the last incident. + /// + public DateTime? LastIncidentAtUtc { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationList.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationList.cs similarity index 89% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationList.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationList.cs index 101b28c7..1dc4c4df 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationList.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationList.cs @@ -1,25 +1,26 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Get a list of applications. - /// - public class GetApplicationList : Query - { - /// - /// Get all applications that the given user have access to - /// - /// - /// - /// 0 = get all applications - /// - /// - public int AccountId { get; set; } - - /// - /// Only list applications that the given account is administrator for. - /// - public bool FilterAsAdmin { get; set; } - } +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Get a list of applications. + /// + [Message] + public class GetApplicationList : Query + { + /// + /// Get all applications that the given user have access to + /// + /// + /// + /// 0 = get all applications + /// + /// + public int AccountId { get; set; } + + /// + /// Only list applications that the given account is administrator for. + /// + public bool FilterAsAdmin { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationOverview.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationOverview.cs new file mode 100644 index 00000000..fe0840dd --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationOverview.cs @@ -0,0 +1,58 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Get stats etc that can be presented as an overview for an application. + /// + [Message] + public class GetApplicationOverview : Query + { + /// + /// Creates a new instance of . + /// + /// + /// applicationId + public GetApplicationOverview(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + ApplicationId = applicationId; + } + + /// + /// Serialization constructor + /// + protected GetApplicationOverview() + { + } + + /// + /// Application id to get an overview for. + /// + public int ApplicationId { get; private set; } + + /// + /// Amount of time to look back (i.e. startdate = DateTime.Now.Substract(WindowSize)) + /// + /// + /// 1 = switch to hours + /// + public int NumberOfDays { get; set; } + + /// + /// Filter on a specific version ("1.1.0") + /// + public string Version { get; set; } + + /// + /// Load chart data. + /// + public bool IncludeChartData { get; set; } = true; + + /// + /// Include summary count per partition. + /// + public bool IncludePartitions { get; set; } = false; + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationOverviewResult.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationOverviewResult.cs similarity index 90% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationOverviewResult.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationOverviewResult.cs index e37f6f9f..03d055de 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationOverviewResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationOverviewResult.cs @@ -1,34 +1,34 @@ -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Result for . - /// - //TODO, move to the web namespace. - public class GetApplicationOverviewResult - { - /// - /// 1 = switch to hours for incidents and reports. - /// - public int Days { get; set; } - - /// - /// One entry for each day - /// - public int[] ErrorReports { get; set; } - - /// - /// One incident count for each day - /// - public int[] Incidents { get; set; } - - /// - /// Statistics summary - /// - public OverviewStatSummary StatSummary { get; set; } - - /// - /// Labels for X axis - /// - public string[] TimeAxisLabels { get; set; } - } +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Result for . + /// + //TODO, move to the web namespace. + public class GetApplicationOverviewResult + { + /// + /// 1 = switch to hours for incidents and reports. + /// + public int Days { get; set; } + + /// + /// One entry for each day + /// + public int[] ErrorReports { get; set; } + + /// + /// One incident count for each day + /// + public int[] Incidents { get; set; } + + /// + /// Statistics summary + /// + public OverviewStatSummary StatSummary { get; set; } + + /// + /// Labels for X axis + /// + public string[] TimeAxisLabels { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeam.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeam.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeam.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeam.cs index 9de2d91f..6037aac6 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeam.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeam.cs @@ -1,34 +1,35 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Get all members of a specific application - /// - public class GetApplicationTeam : Query - { - /// - /// Creates a new instance of . - /// - /// application id - /// applicationId - public GetApplicationTeam(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - } - - /// - /// Serialization constructor - /// - protected GetApplicationTeam() - { - } - - /// - /// Application id - /// - public int ApplicationId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Get all members of a specific application + /// + [Message] + public class GetApplicationTeam : Query + { + /// + /// Creates a new instance of . + /// + /// application id + /// applicationId + public GetApplicationTeam(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + ApplicationId = applicationId; + } + + /// + /// Serialization constructor + /// + protected GetApplicationTeam() + { + } + + /// + /// Application id + /// + public int ApplicationId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamMember.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamMember.cs similarity index 86% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamMember.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamMember.cs index 199f18af..9a56ba6a 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamMember.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamMember.cs @@ -1,25 +1,27 @@ -using System; - -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Item for . - /// - public class GetApplicationTeamMember - { - /// - /// When this person was added to the application (or rather when he accepted the invitation) - /// - public DateTime JoinedAtUtc { get; set; } - - /// - /// Account id - /// - public int UserId { get; set; } - - /// - /// Account name - /// - public string UserName { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Item for . + /// + public class GetApplicationTeamMember + { + /// + /// When this person was added to the application (or rather when he accepted the invitation) + /// + public DateTime JoinedAtUtc { get; set; } + + /// + /// Account id + /// + public int UserId { get; set; } + + /// + /// Account name + /// + public string UserName { get; set; } + + public bool IsAdmin { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamResult.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamResult.cs similarity index 86% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamResult.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamResult.cs index c3b96068..d03dd166 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamResult.cs @@ -1,18 +1,18 @@ -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Result for . - /// - public class GetApplicationTeamResult - { - /// - /// Invited which have not yet accepted the invitation. - /// - public GetApplicationTeamResultInvitation[] Invited { get; set; } - - /// - /// Members - /// - public GetApplicationTeamMember[] Members { get; set; } - } +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Result for . + /// + public class GetApplicationTeamResult + { + /// + /// Invited which have not yet accepted the invitation. + /// + public GetApplicationTeamResultInvitation[] Invited { get; set; } + + /// + /// Members + /// + public GetApplicationTeamMember[] Members { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamResultInvitation.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamResultInvitation.cs similarity index 89% rename from src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamResultInvitation.cs rename to src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamResultInvitation.cs index 114f6d53..70ba1d70 100644 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationTeamResultInvitation.cs +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/GetApplicationTeamResultInvitation.cs @@ -1,25 +1,25 @@ -using System; - -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Item for . - /// - public class GetApplicationTeamResultInvitation - { - /// - /// Address that the invitation was sent to. - /// - public string EmailAddress { get; set; } - - /// - /// When the invitation was sent. - /// - public DateTime InvitedAtUtc { get; set; } - - /// - /// User that sent the invitation. - /// - public string InvitedByUserName { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Item for . + /// + public class GetApplicationTeamResultInvitation + { + /// + /// Address that the invitation was sent to. + /// + public string EmailAddress { get; set; } + + /// + /// When the invitation was sent. + /// + public DateTime InvitedAtUtc { get; set; } + + /// + /// User that sent the invitation. + /// + public string InvitedByUserName { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/OverviewStatSummary.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/OverviewStatSummary.cs new file mode 100644 index 00000000..51aca3bb --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/OverviewStatSummary.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Stats for the last seven days + /// + public class OverviewStatSummary + { + /// + /// Number of followers + /// + public int Followers { get; set; } + + /// + /// Number of incidents + /// + public int Incidents { get; set; } + + /// + /// Number of reports received + /// + public int Reports { get; set; } + + /// + /// Number user feedback items + /// + public int UserFeedback { get; set; } + + + /// + /// Summary per partition + /// + public PartitionOverview[] Partitions { get; set; } + + public DateTime? NewestIncidentReceivedAtUtc { get; set; } + + public DateTime? NewestReportReceivedAtUtc { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Applications/Queries/PartitionOverview.cs b/src/Server/Coderr.Server.Api/Core/Applications/Queries/PartitionOverview.cs new file mode 100644 index 00000000..80d7a9b0 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/Queries/PartitionOverview.cs @@ -0,0 +1,23 @@ +namespace Coderr.Server.Api.Core.Applications.Queries +{ + /// + /// Summary for a partition + /// + public class PartitionOverview + { + /// + /// Name, used when reporting errors. + /// + public string Name { get; set; } + + /// + /// Name to show for users. + /// + public string DisplayName { get; set; } + + /// + /// Amount of unique values for this partition. + /// + public int Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Applications/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Applications/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Applications/ReadMe.md diff --git a/src/Server/Coderr.Server.Api/Core/Applications/TypeOfApplication.cs b/src/Server/Coderr.Server.Api/Core/Applications/TypeOfApplication.cs new file mode 100644 index 00000000..2e59126f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Applications/TypeOfApplication.cs @@ -0,0 +1,38 @@ +namespace Coderr.Server.Api.Core.Applications +{ + /// + /// Kind of application that this is + /// + /// + /// + /// Used to determine how different analytics should be made, like analyzing memory usage (which has to guess the + /// total amount of memory if not included as context information). + /// + /// + /// For instance a OutOfMemoryException isn't as fatal in a mobile application, like it is in a large server + /// application, as the latter is supposed to have large amount of resources. + /// + /// + public enum TypeOfApplication + { + /// + /// Cellphone application + /// + /// + /// + /// An application with limited system resources (memory and usage). + /// + /// + Mobile, + + /// + /// DesktopApplication application (i.e. a windows end user computer) + /// + DesktopApplication, + + /// + /// Server, as a web server or a WCF service. + /// + Server + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Environments/Commands/CreateEnvironment.cs b/src/Server/Coderr.Server.Api/Core/Environments/Commands/CreateEnvironment.cs new file mode 100644 index 00000000..b0e81466 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Environments/Commands/CreateEnvironment.cs @@ -0,0 +1,41 @@ +using System; + +namespace Coderr.Server.Api.Core.Environments.Commands +{ + /// + /// Create an environment. + /// + [Command] + public class CreateEnvironment + { + public CreateEnvironment(int applicationId, string name) + { + if (applicationId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(applicationId)); + } + + ApplicationId = applicationId; + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + protected CreateEnvironment() + { + } + + /// + /// Application that the environment belongs to. + /// + public int ApplicationId { get; private set; } + + /// + /// Do not track incidents in this environment. + /// + public bool DeleteIncidents { get; set; } + + /// + /// Name used when reporting errors using the client library. + /// + public string Name { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Environments/Commands/ResetEnvironment.cs b/src/Server/Coderr.Server.Api/Core/Environments/Commands/ResetEnvironment.cs new file mode 100644 index 00000000..d3f27c3f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Environments/Commands/ResetEnvironment.cs @@ -0,0 +1,32 @@ +using System; +using Coderr.Server.Api.Core.Environments.Queries; + +namespace Coderr.Server.Api.Core.Environments.Commands +{ + /// + /// Delete all incidents in a specific environment + /// + [Command] + public class ResetEnvironment + { + public ResetEnvironment(int applicationId, int environmentId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); + if (environmentId <= 0) throw new ArgumentOutOfRangeException(nameof(environmentId)); + + ApplicationId = applicationId; + EnvironmentId = environmentId; + } + + protected ResetEnvironment() + { + } + + public int ApplicationId { get; private set; } + + /// + /// Environment to reset. Id comes from . + /// + public int EnvironmentId { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Environments/Commands/UpdateEnvironment.cs b/src/Server/Coderr.Server.Api/Core/Environments/Commands/UpdateEnvironment.cs new file mode 100644 index 00000000..4a965659 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Environments/Commands/UpdateEnvironment.cs @@ -0,0 +1,44 @@ +using System; +using Coderr.Server.Api.Core.Environments.Queries; + +namespace Coderr.Server.Api.Core.Environments.Commands +{ + /// + /// Update an environment. + /// + [Command] + public class UpdateEnvironment + { + public UpdateEnvironment(int applicationId, int environmentId) + { + if (applicationId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(applicationId)); + } + + if (environmentId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(environmentId)); + } + + ApplicationId = applicationId; + EnvironmentId = environmentId; + } + + protected UpdateEnvironment() + { + } + + public int ApplicationId { get; private set; } + + /// + /// Do not track incidents in this environment. + /// + public bool DeleteIncidents { get; set; } + + /// + /// Environment to reset. Id comes from . + /// + public int EnvironmentId { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironments.cs b/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironments.cs new file mode 100644 index 00000000..3ce27570 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironments.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Text; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Environments.Queries +{ + /// + /// Get all environments that we've received error reports in + /// + [Message] + public class GetEnvironments : Query + { + public GetEnvironments(int applicationId) + { + if (applicationId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(applicationId)); + } + + ApplicationId = applicationId; + } + + protected GetEnvironments() + { + + } + + /// + /// Fetch all environments for a specific application. + /// + public int ApplicationId { get; private set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironmentsResult.cs b/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironmentsResult.cs new file mode 100644 index 00000000..c3727c11 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironmentsResult.cs @@ -0,0 +1,13 @@ +namespace Coderr.Server.Api.Core.Environments.Queries +{ + /// + /// Result for + /// + public class GetEnvironmentsResult + { + /// + /// Get name of each environment + /// + public GetEnvironmentsResultItem[] Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironmentsResultItem.cs b/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironmentsResultItem.cs new file mode 100644 index 00000000..825fd1d6 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Environments/Queries/GetEnvironmentsResultItem.cs @@ -0,0 +1,23 @@ +namespace Coderr.Server.Api.Core.Environments.Queries +{ + /// + /// Item for . + /// + public class GetEnvironmentsResultItem + { + /// + /// ID + /// + public int Id { get; set; } + + /// + /// Name, like "Production" or "Test" + /// + public string Name { get; set; } + + /// + /// Delete all inbound reports in this environment. + /// + public bool DeleteIncidents { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Feedback/Commands/SubmitFeedback.cs b/src/Server/Coderr.Server.Api/Core/Feedback/Commands/SubmitFeedback.cs similarity index 94% rename from src/Server/OneTrueError.Api/Core/Feedback/Commands/SubmitFeedback.cs rename to src/Server/Coderr.Server.Api/Core/Feedback/Commands/SubmitFeedback.cs index 2ab994db..5449e733 100644 --- a/src/Server/OneTrueError.Api/Core/Feedback/Commands/SubmitFeedback.cs +++ b/src/Server/Coderr.Server.Api/Core/Feedback/Commands/SubmitFeedback.cs @@ -1,106 +1,106 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Feedback.Commands -{ - /// - /// A user that experienced an error have either followed the link to our website to submit an error or have entered it - /// directly into our client library integration. - /// - public class SubmitFeedback : Command, IValidatableObject - { - /// - /// Initializes a new instance of the class. - /// - /// Client side id. - /// The remote address. - /// - /// errorId - /// or - /// remoteAddress - /// - public SubmitFeedback(string errorId, string remoteAddress) - { - if (errorId == null) throw new ArgumentNullException("errorId"); - if (remoteAddress == null) throw new ArgumentNullException("remoteAddress"); - RemoteAddress = remoteAddress; - ErrorId = errorId; - CreatedAtUtc = DateTime.UtcNow; - } - - /// - /// Initializes a new instance of the class. - /// - /// Error report identity. - /// The remote address. - /// - /// remoteAddress - /// - /// reportId - public SubmitFeedback(int reportId, string remoteAddress) - { - if (remoteAddress == null) throw new ArgumentNullException("remoteAddress"); - if (reportId <= 0) throw new ArgumentOutOfRangeException("reportId"); - - RemoteAddress = remoteAddress; - ReportId = reportId; - CreatedAtUtc = DateTime.UtcNow; - } - - /// - /// Serialization constructor - /// - protected SubmitFeedback() - { - } - - /// - /// When the feedback was created in the client library - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// Email address (user want to get status updates) - /// - public string Email { get; set; } - - /// - /// Error id generated in our client library. Used to identify error reports before they have been saved into our - /// system - /// - [Required] - public string ErrorId { get; private set; } - - /// - /// Error description - /// - public string Feedback { get; set; } - - /// - /// IP that the user connected from. either taken from the error report or from the HTTP POST if the UI less client - /// library directed the user to our web site. - /// - public string RemoteAddress { get; set; } - - /// - /// PK from the db entry of the error report. - /// - public int ReportId { get; private set; } - - /// - /// Validate contents of this command - /// - /// validation context - /// Validation errors if any - public IEnumerable Validate(ValidationContext validationContext) - { - if (validationContext == null) throw new ArgumentNullException("validationContext"); - - if (string.IsNullOrEmpty(Email) && string.IsNullOrEmpty(Feedback)) - return new[] {new ValidationResult("Email or Feedback must be given")}; - return new ValidationResult[0]; - } - } +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.Api.Core.Feedback.Commands +{ + /// + /// A user that experienced an error have either followed the link to our website to submit an error or have entered it + /// directly into our client library integration. + /// + [Message] + public class SubmitFeedback + { + /// + /// Initializes a new instance of the class. + /// + /// Client side id. + /// The remote address. + /// + /// errorId + /// or + /// remoteAddress + /// + public SubmitFeedback(string errorId, string remoteAddress) + { + if (errorId == null) throw new ArgumentNullException("errorId"); + if (remoteAddress == null) throw new ArgumentNullException("remoteAddress"); + RemoteAddress = remoteAddress; + ErrorId = errorId; + CreatedAtUtc = DateTime.UtcNow; + } + + /// + /// Initializes a new instance of the class. + /// + /// Error report identity. + /// The remote address. + /// + /// remoteAddress + /// + /// reportId + public SubmitFeedback(int reportId, string remoteAddress) + { + if (remoteAddress == null) throw new ArgumentNullException("remoteAddress"); + if (reportId <= 0) throw new ArgumentOutOfRangeException("reportId"); + + RemoteAddress = remoteAddress; + ReportId = reportId; + CreatedAtUtc = DateTime.UtcNow; + } + + /// + /// Serialization constructor + /// + protected SubmitFeedback() + { + } + + /// + /// When the feedback was created in the client library + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// Email address (user want to get status updates) + /// + public string Email { get; set; } + + /// + /// Error id generated in our client library. Used to identify error reports before they have been saved into our + /// system + /// + [Required] + public string ErrorId { get; private set; } + + /// + /// Error description + /// + public string Feedback { get; set; } + + /// + /// IP that the user connected from. either taken from the error report or from the HTTP POST if the UI less client + /// library directed the user to our web site. + /// + public string RemoteAddress { get; set; } + + /// + /// PK from the db entry of the error report. + /// + public int ReportId { get; private set; } + + /// + /// Validate contents of this command + /// + /// validation context + /// Validation errors if any + public IEnumerable Validate(ValidationContext validationContext) + { + if (validationContext == null) throw new ArgumentNullException("validationContext"); + + if (string.IsNullOrEmpty(Email) && string.IsNullOrEmpty(Feedback)) + return new[] {new ValidationResult("Email or Feedback must be given")}; + return new ValidationResult[0]; + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Feedback/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Feedback/NamespaceDoc.cs new file mode 100644 index 00000000..0bf4ef2d --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Feedback/NamespaceDoc.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Feedback +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// Feedback / error description written by the user when the exception was caught. + /// + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Feedback/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Feedback/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Feedback/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Feedback/ReadMe.md diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Commands/AssignIncident.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/AssignIncident.cs new file mode 100644 index 00000000..a751f8a9 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/AssignIncident.cs @@ -0,0 +1,48 @@ +using System; + +namespace Coderr.Server.Api.Core.Incidents.Commands +{ + /// + /// Start working on an incident. + /// + [Command] + public class AssignIncident + { + /// + /// Creates new instance of . + /// + /// Incident being assigned + /// Id of the user that got assigned to this incident + /// Id of the user that assigned this incident, 0 for system requests + public AssignIncident(int incidentId, int assignedTo, int assignedBy) + { + if (assignedTo <= 0) throw new ArgumentOutOfRangeException(nameof(assignedTo)); + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + if (assignedBy < 0) throw new ArgumentOutOfRangeException(nameof(assignedBy)); + + IncidentId = incidentId; + AssignedTo = assignedTo; + AssignedBy = assignedBy; + } + + /// + /// Id of the user that assigned this incident. + /// + public int AssignedBy { get; private set; } + + /// + /// Id of the user that got assigned to this incident. + /// + public int AssignedTo { get; private set; } + + /// + /// Incident being assigned. + /// + public int IncidentId { get; private set; } + + /// + /// Optionally specify when the incident was assigned. Default = now. + /// + public DateTime? AssignedAtUtc { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Commands/CloseIncident.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/CloseIncident.cs similarity index 81% rename from src/Server/OneTrueError.Api/Core/Incidents/Commands/CloseIncident.cs rename to src/Server/Coderr.Server.Api/Core/Incidents/Commands/CloseIncident.cs index b808dbfb..7158d382 100644 --- a/src/Server/OneTrueError.Api/Core/Incidents/Commands/CloseIncident.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/CloseIncident.cs @@ -1,68 +1,78 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Incidents.Commands -{ - /// - /// Close incident (i.e. we have corrected the issue) - /// - public class CloseIncident : Command - { - /// - /// Creates a new instance of . - /// - /// Markdown formatted string detailing how we solved this incident. - /// Incident that was solved. - public CloseIncident(string solution, int incidentId) - { - if (solution == null) throw new ArgumentNullException("solution"); - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - Solution = solution; - } - - /// - /// Serialization constructor. - /// - protected CloseIncident() - { - } - - /// - /// Can send notifications to everyone which has reported exceptions through our system. - /// - public bool CanSendNotification { get; set; } - - /// - /// - public int IncidentId { get; private set; } - - /// - /// Text to send as email body - /// - public string NotificationText { get; set; } - - /// - /// Title of outbound notification. - /// - public string NotificationTitle { get; set; } - - /// - /// If this solution can be shared with other OTE customers. - /// - public bool ShareSolution { get; set; } - - /// - /// How the incident has been fixed. - /// - public string Solution { get; set; } - - /// - /// User that closed the incident - /// - /// - /// Need to be named "UserId" so that the CQS mapper can add the logged in user id - /// - public int UserId { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Incidents.Commands +{ + /// + /// Close incident (i.e. we have corrected the issue) + /// + [Message] + public class CloseIncident + { + /// + /// Creates a new instance of . + /// + /// Markdown formatted string detailing how we solved this incident. + /// Incident that was solved. + public CloseIncident(string solution, int incidentId) + { + if (solution == null) throw new ArgumentNullException("solution"); + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + Solution = solution; + } + + /// + /// Serialization constructor. + /// + protected CloseIncident() + { + } + + /// + /// Which version that incident is solved in (like "1.2.1"). + /// + public string ApplicationVersion { get; set; } + + /// + /// Can send notifications to everyone which has reported exceptions through our system. + /// + public bool CanSendNotification { get; set; } + + /// + /// + public int IncidentId { get; private set; } + + /// + /// Text to send as email body + /// + public string NotificationText { get; set; } + + /// + /// Title of outbound notification. + /// + public string NotificationTitle { get; set; } + + /// + /// If this solution can be shared with other OTE customers. + /// + public bool ShareSolution { get; set; } + + /// + /// How the incident has been fixed. + /// + public string Solution { get; set; } + + /// + /// User that closed the incident + /// + /// + /// Need to be named "UserId" so that the CQS mapper can add the logged in user id + /// + public int UserId { get; set; } + + /// + /// When incident was closed (optional, "now" will be used when not specified) + /// + public DateTime? ClosedAtUtc { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Commands/DeleteIncident.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/DeleteIncident.cs new file mode 100644 index 00000000..4b88f2a6 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/DeleteIncident.cs @@ -0,0 +1,31 @@ +using System; + +namespace Coderr.Server.Api.Core.Incidents.Commands +{ + /// + /// Delete incidents (old, created in dev environment etc) + /// + [Message] + public class DeleteIncident + { + /// + /// Creates a new instance of . + /// + /// incident to delete + /// should be "yes" + public DeleteIncident(int incidentId, string areYouSure) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + if (areYouSure != "yes") + throw new ArgumentOutOfRangeException(nameof(areYouSure), areYouSure, "Must be 'yes'."); + IncidentId = incidentId; + AreYouSure = areYouSure; + } + + public int IncidentId { get; private set; } + public int? UserId { get; set; } + + public DateTime? DeletedAtUtc { get; set; } + public string AreYouSure { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Commands/IgnoreIncident.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/IgnoreIncident.cs similarity index 86% rename from src/Server/OneTrueError.Api/Core/Incidents/Commands/IgnoreIncident.cs rename to src/Server/Coderr.Server.Api/Core/Incidents/Commands/IgnoreIncident.cs index 77ef4389..9ab4eff3 100644 --- a/src/Server/OneTrueError.Api/Core/Incidents/Commands/IgnoreIncident.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/IgnoreIncident.cs @@ -1,40 +1,40 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Incidents.Commands -{ - /// - /// Ignore incident - /// - public class IgnoreIncident : Command - { - /// - /// Creates a new instance of . - /// - /// incident id - /// incidentId - public IgnoreIncident(int incidentId) - { - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - } - - - /// - /// Serialization constructor - /// - protected IgnoreIncident() - { - } - - /// - /// Incident to ignore - /// - public int IncidentId { get; private set; } - - /// - /// Person that ignored the report. - /// - public int UserId { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Incidents.Commands +{ + /// + /// Ignore incident + /// + [Message] + public class IgnoreIncident + { + /// + /// Creates a new instance of . + /// + /// incident id + /// incidentId + public IgnoreIncident(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + } + + + /// + /// Serialization constructor + /// + protected IgnoreIncident() + { + } + + /// + /// Incident to ignore + /// + public int IncidentId { get; private set; } + + /// + /// Person that ignored the report. + /// + public int UserId { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Commands/NotifySubscribers.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/NotifySubscribers.cs new file mode 100644 index 00000000..35413857 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/NotifySubscribers.cs @@ -0,0 +1,37 @@ +using System; + +namespace Coderr.Server.Api.Core.Incidents.Commands +{ + /// + /// Notify all users that have subscribed on an incident. + /// + [Command] + public class NotifySubscribers + { + public NotifySubscribers(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + IncidentId = incidentId; + } + + protected NotifySubscribers() + { + + } + + /// + /// Incident id + /// + public int IncidentId { get; private set; } + /// + /// Text to send as email body + /// + public string Body { get; set; } + + /// + /// Title of outbound notification. + /// + public string Title { get; set; } + + } +} diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Commands/ReOpenIncident.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/ReOpenIncident.cs new file mode 100644 index 00000000..5db356b6 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Commands/ReOpenIncident.cs @@ -0,0 +1,40 @@ +using System; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + +namespace Coderr.Server.Api.Core.Incidents.Commands +{ + /// + /// An incident which has either been closed or ignored is marked as active again + /// + [Message] + public class ReOpenIncident + { + /// + /// Creates a new instance of . + /// + /// incident to reopen + public ReOpenIncident(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + IncidentId = incidentId; + } + + /// + /// Serialization constructor + /// + protected ReOpenIncident() + { + } + + /// + /// Incident to reopen + /// + public int IncidentId { get; private set; } + + /// + /// User requesting item to be reopened. + /// + public int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentAssigned.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentAssigned.cs new file mode 100644 index 00000000..bc2e19e2 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentAssigned.cs @@ -0,0 +1,55 @@ +using System; + +namespace Coderr.Server.Api.Core.Incidents.Events +{ + /// + /// Someone was assigned to an incident + /// + [Event] + public class IncidentAssigned + { + /// + /// Creates a new instance of . + /// + /// Incident being assigned + /// User assigning the incident + /// User that should start working with the incident + /// When incident was assigned + public IncidentAssigned(int incidentId, int assignedById, int assignedToId, DateTime assignedAtUtc) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + if (assignedById <= 0) throw new ArgumentOutOfRangeException(nameof(assignedById)); + if (assignedToId <= 0) throw new ArgumentOutOfRangeException(nameof(assignedToId)); + + IncidentId = incidentId; + AssignedById = assignedById; + AssignedToId = assignedToId; + AssignedAtUtc = assignedAtUtc; + } + + protected IncidentAssigned() + { + + } + + /// + /// User assigning the incident (delegate work) + /// + public int AssignedById { get; private set; } + + /// + /// User that should start working with the incident + /// + public int AssignedToId { get; private set; } + + /// + /// When the incident was assigned (client side) + /// + public DateTime AssignedAtUtc { get; private set; } + + /// + /// Incident being assigned + /// + public int IncidentId { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentClosed.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentClosed.cs new file mode 100644 index 00000000..91c8cfda --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentClosed.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Coderr.Server.Api.Core.Incidents.Events +{ + [Event] + public class IncidentClosed + { + public IncidentClosed(int applicationId, int incidentId, int userId, string solution, string applicationVersion, DateTime closedAtUtc) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId)); + if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); + + ApplicationId = applicationId; + IncidentId = incidentId; + ClosedById = userId; + Solution = solution; + ApplicationVersion = applicationVersion; + ClosedAtUtc = closedAtUtc; + } + + protected IncidentClosed() + { + + } + + public int ClosedById { get; private set; } + public int IncidentId { get;private set; } + public string Solution { get;private set; } + public string ApplicationVersion { get; private set; } + public int ApplicationId { get; set; } + public DateTime ClosedAtUtc { get; private set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentDeleted.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentDeleted.cs new file mode 100644 index 00000000..4cac5799 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentDeleted.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Coderr.Server.Api.Core.Incidents.Events +{ + [Event] + public class IncidentDeleted + { + public IncidentDeleted(int incidentId, int userId, DateTime deletedAtUtc) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId)); + + IncidentId = incidentId; + DeletedById = userId; + DeletedAtUtc = deletedAtUtc; + } + + protected IncidentDeleted() + { + + } + + public int IncidentId { get;private set; } + + public int DeletedById { get; set; } + public DateTime DeletedAtUtc { get; private set; } + } +} diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Events/IncidentIgnored.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentIgnored.cs similarity index 76% rename from src/Server/OneTrueError.Api/Core/Incidents/Events/IncidentIgnored.cs rename to src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentIgnored.cs index ea587cc7..ffe14a2c 100644 --- a/src/Server/OneTrueError.Api/Core/Incidents/Events/IncidentIgnored.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Events/IncidentIgnored.cs @@ -1,52 +1,57 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Incidents.Events -{ - /// - /// Our user have configured that all new reports for this incident should be ignored - /// - public class IncidentIgnored : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// incident being ignored - /// account ignoring the incident - /// userName for the given account - /// - /// - public IncidentIgnored(int incidentId, int accountId, string userName) - { - if (userName == null) throw new ArgumentNullException("userName"); - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - - IncidentId = incidentId; - AccountId = accountId; - UserName = userName; - } - - /// - /// Serialization constructor - /// - protected IncidentIgnored() - { - } - - /// - /// User that configured ignore. - /// - public int AccountId { get; set; } - - /// - /// Incident id - /// - public int IncidentId { get; set; } - - /// - /// Name of the user. - /// - public string UserName { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Incidents.Events +{ + /// + /// Our user have configured that all new reports for this incident should be ignored + /// + [Message] + public class IncidentIgnored + { + /// + /// Creates a new instance of . + /// + /// Application that the incident belongs to. + /// incident being ignored + /// account ignoring the incident + /// userName for the given account + /// + /// + public IncidentIgnored(int applicationId, int incidentId, int accountId, string userName) + { + if (userName == null) throw new ArgumentNullException("userName"); + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); + if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); + + ApplicationId = applicationId; + IncidentId = incidentId; + AccountId = accountId; + UserName = userName; + } + + /// + /// Serialization constructor + /// + protected IncidentIgnored() + { + } + + /// + /// User that configured ignore. + /// + public int AccountId { get; set; } + + public int ApplicationId { get; } + + /// + /// Incident id + /// + public int IncidentId { get; set; } + + /// + /// Name of the user. + /// + public string UserName { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/IncidentOrder.cs b/src/Server/Coderr.Server.Api/Core/Incidents/IncidentOrder.cs new file mode 100644 index 00000000..5e0eeac7 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/IncidentOrder.cs @@ -0,0 +1,28 @@ +namespace Coderr.Server.Api.Core.Incidents +{ + /// + /// How incidents should be ordered in a list + /// + public enum IncidentOrder + { + /// + /// Newest incidents first + /// + Newest, + + /// + /// The one that recieved a report most recent. + /// + LatestReport, + + /// + /// The incident with the highest number of reports + /// + MostReports, + + /// + /// The incidents with the most given feedback + /// + MostFeedback + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/IncidentSummaryDTO.cs b/src/Server/Coderr.Server.Api/Core/Incidents/IncidentSummaryDTO.cs new file mode 100644 index 00000000..73e9013f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/IncidentSummaryDTO.cs @@ -0,0 +1,80 @@ +using System; + +namespace Coderr.Server.Api.Core.Incidents +{ + /// + /// A small summary of an incident, typically used to list incidents. + /// + public class IncidentSummaryDTO + { + /// + /// Creates a new instance of . + /// + /// incident id + /// incident name + /// name + /// incident id + public IncidentSummaryDTO(int id, string name) + { + if (name == null) throw new ArgumentNullException("name"); + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + + Id = id; + Name = name; + } + + /// + /// Serialization constructor + /// + protected IncidentSummaryDTO() + { + } + + /// + /// Application that the incident belongs to + /// + public int ApplicationId { get; set; } + + /// + /// Name of that application + /// + public string ApplicationName { get; set; } + + /// + /// When the incident was created (when we received the first report). + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// Incident id + /// + public int Id { get; set; } + + /// + /// Incident was closed but then received a new error report. + /// + public bool IsReOpened { get; set; } + + /// + /// someone is assigned to this incident + /// + public int? AssignedToUserId { get; set; } + + /// + /// Update is both when the incident was open/closed and when we received a new report. TODO: Should be refactored into + /// two fields. + /// + public DateTime LastUpdateAtUtc { get; set; } + + /// + /// Incident name (typically first line of the exception message) + /// + public string Name { get; set; } + + /// + /// Number of reports that we've received. Should be the total amount (including those that have been deleted due to + /// retention days). + /// + public int ReportCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Incidents/NamespaceDoc.cs new file mode 100644 index 00000000..e4c523d5 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/NamespaceDoc.cs @@ -0,0 +1,17 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Incidents +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// All instances of the same exception are grouped together into an incident (i.e. even if the same exception is + /// thrown 100 000 it's still the same incident). + /// + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidents.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidents.cs new file mode 100644 index 00000000..a43cbdec --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidents.cs @@ -0,0 +1,145 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Find incidents + /// + /// + /// + /// Default query is only open incidents with 20 items per page. + /// + /// + [Message] + public class FindIncidents : Query + { + /// + /// Creates a new instance of . + /// + public FindIncidents() + { + MaxDate = DateTime.MaxValue; + ItemsPerPage = 20; + } + + + /// + /// Find incidents assigned to the specified user + /// + public int AssignedToId { get; set; } + + /// + /// Empty = find for all applications + /// + /// + /// The application identifier. + /// + public int[] ApplicationIds { get; set; } + + /// + /// Find an incident with a specific collection + /// + /// + /// + /// Wildcard search is allowed, i.e. "http*" + /// + /// + public string ContextCollectionName { get; set; } + + /// + /// Find an incident with a specific property + /// + /// + /// + /// Wildcard search is allowed, i.e. "http*" + /// + /// + public string ContextCollectionPropertyName { get; set; } + + /// + /// Find an incident with a specific property value + /// + /// + /// + /// Wildcard search is allowed, i.e. "http*" + /// + /// + public string ContextCollectionPropertyValue { get; set; } + + /// + /// Which environments we should search in. + /// + public int[] EnvironmentIds { get; set; } + + /// + /// Will be searched in incident.message and report.stacktrace. + /// + public string FreeText { get; set; } + + /// + /// Been assigned to someone + /// + public bool IsAssigned { get; set; } + + /// + /// Include closed incidents + /// + public bool IsClosed { get; set; } + + /// + /// Include ignored incidents + /// + public bool IsIgnored { get; set; } + + /// + /// Incidents that have not been assigned to someone (or closed/ignored). + /// + public bool IsNew { get; set; } + + /// + /// Number of items per page. + /// + public int ItemsPerPage { get; set; } + + /// + /// End of period + /// + public DateTime MaxDate { get; set; } + + /// + /// Start of period + /// + public DateTime MinDate { get; set; } + + /// + /// Page to fetch (one based index) + /// + public int PageNumber { get; set; } + + /// + /// Include reopened incidents + /// + public bool ReOpened { get; set; } + + /// + /// Sort order + /// + public bool SortAscending { get; set; } + + /// + /// Sort type + /// + public IncidentOrder SortType { get; set; } + + /// + /// Incident should have all the specified tags + /// + public string[] Tags { get; set; } + + /// + /// Version (in the form of a version string i.e. "1.2.1") + /// + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResult.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResult.cs new file mode 100644 index 00000000..fe00deed --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResult.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Result for . + /// + public class FindIncidentsResult + { + /// + /// Items + /// + public IReadOnlyList Items { get; set; } + + /// + /// Page number (one based index) + /// + public int PageNumber { get; set; } + + /// + /// Items returned for this page + /// + public int PageSize { get; set; } + + /// + /// Total number of items + /// + public int TotalCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResultItem.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResultItem.cs new file mode 100644 index 00000000..841a1acb --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/FindIncidentsResultItem.cs @@ -0,0 +1,104 @@ +using System; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Item for . + /// + public class FindIncidentsResultItem + { + /// + /// Creates new instance of . + /// + /// incident id + /// incident name + public FindIncidentsResultItem(int id, string name) + { + if (name == null) throw new ArgumentNullException("name"); + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + Id = id; + Name = name; + } + + /// + /// Serialization constructor + /// + protected FindIncidentsResultItem() + { + } + + /// + /// Id of the application that this incident belongs to + /// + public int ApplicationId { get; set; } + + /// + /// Name of the application that this incident belongs to + /// + public string ApplicationName { get; set; } + + /// + /// When the first report was received. + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// When the incident was assigned to someone. + /// + public DateTime? AssignedAtUtc { get; set; } + + /// + /// Incident id + /// + public int Id { get; private set; } + + + /// + /// Incident have been automatically opened again after being closed by a user. + /// + public bool IsReOpened { get; set; } + + /// + /// When we received the last report. + /// + public DateTime LastReportReceivedAtUtc { get; set; } + + /// + /// When someone updated this incident (assigned/closed etc). + /// + public DateTime LastUpdateAtUtc { get; set; } + + /// + /// Incident name + /// + public string Name { set; get; } + + /// + /// Total number of received reports (increased even if the number of stored reports are at the limit) + /// + public int ReportCount { get; set; } + + /// + /// Stores the state temporary to be able to assigned the bool fields + /// + [IgnoreField] + public int IncidentState { get; set; } + + /// + /// Ignore future reports for this incident (i.e. no notifications, do not store new reports etc). + /// + /// + /// + /// Report counter will still be updated. + /// + /// + public bool IsIgnored => IncidentState == 2; + + /// + /// Incident has been marked as solved (i.e. closed) + /// + public bool IsSolved => IncidentState == 3; + + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollection.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollection.cs new file mode 100644 index 00000000..69cd770a --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollection.cs @@ -0,0 +1,37 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Fetch a specific collection from all reports, sorted in descending order. + /// + public class GetCollection : Query + { + public GetCollection(int incidentId, string collectionName) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + IncidentId = incidentId; + CollectionName = collectionName ?? throw new ArgumentNullException(nameof(collectionName)); + } + + protected GetCollection() + { + } + + /// + /// Collection name like "ErrorProperties" or "HttpRequest". + /// + public string CollectionName { get; private set; } + + /// + /// Incident that the collection belongs to. + /// + public int IncidentId { get; private set; } + + /// + /// Collection limit. + /// + public int MaxNumberOfCollections { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResult.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResult.cs new file mode 100644 index 00000000..01ecb34b --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResult.cs @@ -0,0 +1,13 @@ +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Result for . + /// + public class GetCollectionResult + { + /// + /// Fetched collections. + /// + public GetCollectionResultItem[] Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResultItem.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResultItem.cs new file mode 100644 index 00000000..6952e2f5 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetCollectionResultItem.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// A collection + /// + public class GetCollectionResultItem + { + /// + /// Properties in the collection (for instance "Url" if this is the HTTP request). + /// + public Dictionary Properties { get; set; } + + /// + /// Date for the report that this collection is for. + /// + public DateTime ReportDate { get; set; } + + /// + /// Id of the report that this collection was received in. + /// + public int ReportId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncident.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncident.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncident.cs rename to src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncident.cs index 781059ae..0c18d101 100644 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncident.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncident.cs @@ -1,35 +1,36 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Get incident query - /// - public class GetIncident : Query - { - /// - /// Creates a new instance of . - /// - /// incident id - /// incidentId - public GetIncident(int incidentId) - { - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - } - - - /// - /// Serialization constructor. - /// - protected GetIncident() - { - } - - /// - /// Incident id - /// - public int IncidentId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Get incident query + /// + [Message] + public class GetIncident : Query + { + /// + /// Creates a new instance of . + /// + /// incident id + /// incidentId + public GetIncident(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + } + + + /// + /// Serialization constructor. + /// + protected GetIncident() + { + } + + /// + /// Incident id + /// + public int IncidentId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentForClosePage.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentForClosePage.cs new file mode 100644 index 00000000..f11d52f6 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentForClosePage.cs @@ -0,0 +1,35 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Get incident information tailored for the close page. + /// + [Message] + public class GetIncidentForClosePage : Query + { + /// + /// Serialization constructor + /// + protected GetIncidentForClosePage() + { + } + + /// + /// Creates a new instance of . + /// + /// incident id + /// incidentId + public GetIncidentForClosePage(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + } + + /// + /// Incident id + /// + public int IncidentId { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentForClosePageResult.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentForClosePageResult.cs similarity index 87% rename from src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentForClosePageResult.cs rename to src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentForClosePageResult.cs index 188d7441..6d8e4903 100644 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentForClosePageResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentForClosePageResult.cs @@ -1,18 +1,18 @@ -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Result for . - /// - public class GetIncidentForClosePageResult - { - /// - /// A summary of the incident - /// - public string Description { get; set; } - - /// - /// Number of update subscribers (i.e. users that want status updates). - /// - public int SubscriberCount { get; set; } - } +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Result for . + /// + public class GetIncidentForClosePageResult + { + /// + /// A summary of the incident + /// + public string Description { get; set; } + + /// + /// Number of update subscribers (i.e. users that want status updates). + /// + public int SubscriberCount { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentResult.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentResult.cs new file mode 100644 index 00000000..2706993d --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentResult.cs @@ -0,0 +1,171 @@ +using System; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Keeps track of all occurrences of a single incident (i.e. error reports which generates the same hash code) + /// + public class GetIncidentResult + { + private string _description; + + + /// + /// Application that the incident belongs to + /// + public int ApplicationId { get; private set; } + + /// + /// When it was assigned to the person. + /// + public DateTime? AssignedAtUtc { get; set; } + + /// + /// User name of the person that this incident is assigned to. + /// + public string AssignedTo { get; set; } + + /// + /// User assigned to the incident. + /// + public int? AssignedToId { get; set; } + + /// + /// Context collection names. + /// + public string[] ContextCollections { get; set; } + + /// + /// When the incident was created (when we received the first exception). + /// + public DateTime CreatedAtUtc { get; private set; } + + /// + /// Daily statistics. + /// + public ReportDay[] DayStatistics { get; set; } + + /// + /// Error description (exception message) + /// + public string Description + { + get + { + if (string.IsNullOrEmpty(_description)) + return "Ooops Error!"; + + return _description; + } + set => _description = value; + } + + /// + /// facts + /// + public QuickFact[] Facts { get; set; } + + /// + /// Full name of the exception message. + /// + public string FullName { get; private set; } + + /// + /// Used to identify this incident when the hash code is the same as for other incidents. + /// + /// + public string HashCodeIdentifier { get; private set; } + + /// + /// primary key + /// + public int Id { get; private set; } + + /// + /// Stores the state temporary to be able to assigned the bool fields + /// + [IgnoreField] + public int IncidentState { get; set; } + + /// + /// Ignore future reports for this incident (i.e. no notifications, do not store new reports etc). + /// + /// + /// + /// Report counter will still be updated. + /// + /// + public bool IsIgnored => IncidentState == 2; + + /// + /// If the incident was closed and then received error reports again. + /// + public bool IsReOpened { get; set; } + + /// + /// Share solution with the Coderr community. + /// + public bool IsSolutionShared { get; set; } + + /// + /// Incident has been marked as solved (i.e. closed) + /// + public bool IsSolved => IncidentState == 3; + + /// + /// When we received the last report for this incident. + /// + public DateTime LastReportReceivedAtUtc { get; set; } + + /// + /// Solution written last time (if is true). + /// + public DateTime PreviousSolutionAtUtc { get; set; } + + /// + /// Date if is true. + /// + public DateTime ReOpenedAtUtc { get; set; } + + + /// + /// Number of reports received to date. + /// + public int ReportCount { get; set; } + + /// + /// Generated hash code + /// + public string ReportHashCode { get; private set; } + + /// + /// How the incident was solved (the last time) + /// + public string Solution { get; set; } + + /// + /// When the incident was closed/solved. + /// + public DateTime SolvedAtUtc { get; set; } + + /// + /// Stack trace. + /// + public string StackTrace { get; set; } + + /// + /// Identified StackOverflow tags. + /// + public string[] Tags { get; set; } + + /// + /// When the incident was updated (either a new report or changes to the actual incident) + /// + public DateTime UpdatedAtUtc { get; private set; } + + public SuggestedIncidentSolution[] SuggestedSolutions { get; set; } + public HighlightedContextData[] HighlightedContextData { get; set; } + + public RelatedIncident[] RelatedIncidents { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentStatistics.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentStatistics.cs similarity index 75% rename from src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentStatistics.cs rename to src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentStatistics.cs index 057266e1..72a65489 100644 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentStatistics.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentStatistics.cs @@ -1,23 +1,24 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Get statistics (i.e. history for a certain period of time) - /// - public class GetIncidentStatistics : Query - { - /// - /// Incident to get stats for. - /// - public int IncidentId { get; set; } - - /// - /// Amount of time to look back (i.e. startdate = DateTime.Now.Substract(WindowSize)) - /// - /// - /// 1 = switch to hours - /// - public int NumberOfDays { get; set; } - } +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Get statistics (i.e. history for a certain period of time) + /// + [Message] + public class GetIncidentStatistics : Query + { + /// + /// Incident to get stats for. + /// + public int IncidentId { get; set; } + + /// + /// Amount of time to look back (i.e. start date = DateTime.Now.Substract(WindowSize)) + /// + /// + /// 1 = switch to hours + /// + public int NumberOfDays { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentStatisticsResult.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentStatisticsResult.cs similarity index 85% rename from src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentStatisticsResult.cs rename to src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentStatisticsResult.cs index f5b35107..179bed5c 100644 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentStatisticsResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/GetIncidentStatisticsResult.cs @@ -1,19 +1,19 @@ -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Result for . - /// - public class GetIncidentStatisticsResult - { - /// - /// Labels (dates) - /// - public string[] Labels { get; set; } - - - /// - /// Incident counts per date - /// - public int[] Values { get; set; } - } +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Result for . + /// + public class GetIncidentStatisticsResult + { + /// + /// Labels (dates) + /// + public string[] Labels { get; set; } + + + /// + /// Incident counts per date + /// + public int[] Values { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/HighlightedContextData.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/HighlightedContextData.cs new file mode 100644 index 00000000..e5ce4864 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/HighlightedContextData.cs @@ -0,0 +1,36 @@ +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Context data that can help the developer to directly understand why the exception happened. + /// + /// + /// + /// For instance for Page Not Found this is the URL and the Referrer. + /// + /// + public class HighlightedContextData + { + /// + /// Why this data helps and what it means. + /// + public string Description { get; set; } + + /// + /// Name ("UrlReferrer", "Url", "HttpCode" etc). + /// + public string Name { get; set; } + + /// + /// Optional url that the user can click on to get more information + /// + public string Url { get; set; } + + /// + /// Value to show (one per report or only from the latest report). + /// + /// + /// Values should be sorted i priority order (first item will be displayed directly) + /// + public string[] Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/QuickFact.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/QuickFact.cs new file mode 100644 index 00000000..ebb23590 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/QuickFact.cs @@ -0,0 +1,36 @@ +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// Quick fact for incidents. + /// + /// + /// + /// For instance number of reports, when the incident was created, number of affected users etc. + /// + /// + public class QuickFact + { + /// + /// what this fact displays + /// + public string Description { get; set; } + + /// + /// Fact title (heading) + /// + public string Title { get; set; } + + /// + /// Optional url to get more information. + /// + public string Url { get; set; } + + /// + /// Value to show + /// + /// + /// For multiple values; separate them with semi colons. + /// + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/RelatedIncident.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/RelatedIncident.cs new file mode 100644 index 00000000..fa5edd50 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/RelatedIncident.cs @@ -0,0 +1,13 @@ +using System; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + public class RelatedIncident + { + public int ApplicationId { get; set; } + public string ApplicationName { get; set; } + public DateTime CreatedAtUtc { get; set; } + public int IncidentId { get; set; } + public string Title { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/ReportDay.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/ReportDay.cs similarity index 84% rename from src/Server/OneTrueError.Api/Core/Incidents/Queries/ReportDay.cs rename to src/Server/Coderr.Server.Api/Core/Incidents/Queries/ReportDay.cs index f8267420..1a4b2f8c 100644 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/ReportDay.cs +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/ReportDay.cs @@ -1,20 +1,20 @@ -using System; - -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// A day in our statistics - /// - public class ReportDay - { - /// - /// Number of items this day - /// - public int Count { get; set; } - - /// - /// Date - /// - public DateTime Date { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// A day in our statistics + /// + public class ReportDay + { + /// + /// Number of items this day + /// + public int Count { get; set; } + + /// + /// Date + /// + public DateTime Date { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Incidents/Queries/SuggestedIncidentSolution.cs b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/SuggestedIncidentSolution.cs new file mode 100644 index 00000000..6bc4be19 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Incidents/Queries/SuggestedIncidentSolution.cs @@ -0,0 +1,19 @@ +namespace Coderr.Server.Api.Core.Incidents.Queries +{ + /// + /// A suggested solution for the incident + /// + public class SuggestedIncidentSolution + { + /// + /// Common reasons to why this exception is thrown. + /// + public string Reason { get; set; } + + + /// + /// How the incident can be solved. + /// + public string SuggestedSolution { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Incidents/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Incidents/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Incidents/ReadMe.md diff --git a/src/Server/Coderr.Server.Api/Core/Invitations/Commands/DeleteInvitation.cs b/src/Server/Coderr.Server.Api/Core/Invitations/Commands/DeleteInvitation.cs new file mode 100644 index 00000000..3a81a5a2 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Invitations/Commands/DeleteInvitation.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Coderr.Server.Api.Core.Invitations.Commands +{ + [Message] + public class DeleteInvitation + { + public int ApplicationId { get; set; } + public string InvitedEmailAddress { get; set; } + + } +} diff --git a/src/Server/OneTrueError.Api/Core/Invitations/Commands/InviteUser.cs b/src/Server/Coderr.Server.Api/Core/Invitations/Commands/InviteUser.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Invitations/Commands/InviteUser.cs rename to src/Server/Coderr.Server.Api/Core/Invitations/Commands/InviteUser.cs index adec93de..ec8e4ba4 100644 --- a/src/Server/OneTrueError.Api/Core/Invitations/Commands/InviteUser.cs +++ b/src/Server/Coderr.Server.Api/Core/Invitations/Commands/InviteUser.cs @@ -1,58 +1,58 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Invitations.Commands -{ - /// - /// Invite a user to participate in an application. - /// - /// - /// - /// Will send an invitation email to the user if the email is not for a registered user, otherwise we'll just - /// add the user as a member of the specified application. - /// - /// - public class InviteUser : Command - { - /// - /// Create a new instance of . - /// - /// Application to gain access to - /// Email for the given user. - public InviteUser(int applicationId, string emailAddress) - { - if (emailAddress == null) throw new ArgumentNullException("emailAddress"); - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - EmailAddress = emailAddress; - } - - /// - /// Serialization constructor - /// - protected InviteUser() - { - } - - /// - /// Application that the user will get access to. - /// - public int ApplicationId { get; set; } - - /// - /// Email to invited user. - /// - public string EmailAddress { get; private set; } - - /// - /// A text written by the user to describe why the invite was sent. - /// - public string Text { get; set; } - - /// - /// User that invited - /// - [IgnoreField] - public int UserId { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Invitations.Commands +{ + /// + /// Invite a user to participate in an application. + /// + /// + /// + /// Will send an invitation email to the user if the email is not for a registered user, otherwise we'll just + /// add the user as a member of the specified application. + /// + /// + [Message] + public class InviteUser + { + /// + /// Create a new instance of . + /// + /// Application to gain access to + /// Email for the given user. + public InviteUser(int applicationId, string emailAddress) + { + if (emailAddress == null) throw new ArgumentNullException("emailAddress"); + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + ApplicationId = applicationId; + EmailAddress = emailAddress; + } + + /// + /// Serialization constructor + /// + protected InviteUser() + { + } + + /// + /// Application that the user will get access to. + /// + public int ApplicationId { get; set; } + + /// + /// Email to invited user. + /// + public string EmailAddress { get; private set; } + + /// + /// A text written by the user to describe why the invite was sent. + /// + public string Text { get; set; } + + /// + /// User that invited + /// + [IgnoreField] + public int UserId { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Invitations/Events/InvitationDeleted.cs b/src/Server/Coderr.Server.Api/Core/Invitations/Events/InvitationDeleted.cs new file mode 100644 index 00000000..bc1e2249 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Invitations/Events/InvitationDeleted.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Coderr.Server.Api.Core.Invitations.Events +{ + [Event] + public class InvitationDeleted + { + public int InvitationId { get; set; } + public string InvitedEmailAddress { get; set; } + public int[] ApplicationIds { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Core/Invitations/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Invitations/NamespaceDoc.cs new file mode 100644 index 00000000..0c53d9b7 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Invitations/NamespaceDoc.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Invitations +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// Invitations are sent on application basis. + /// + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Invitations/Queries/GetInvitationByKey.cs b/src/Server/Coderr.Server.Api/Core/Invitations/Queries/GetInvitationByKey.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Invitations/Queries/GetInvitationByKey.cs rename to src/Server/Coderr.Server.Api/Core/Invitations/Queries/GetInvitationByKey.cs index 03727044..69faafc2 100644 --- a/src/Server/OneTrueError.Api/Core/Invitations/Queries/GetInvitationByKey.cs +++ b/src/Server/Coderr.Server.Api/Core/Invitations/Queries/GetInvitationByKey.cs @@ -1,34 +1,35 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Invitations.Queries -{ - /// - /// Get invitation by using the emailed invitation key - /// - public class GetInvitationByKey : Query - { - /// - /// Creates a new instance of . - /// - /// Emailed key - /// invitationKey - public GetInvitationByKey(string invitationKey) - { - if (invitationKey == null) throw new ArgumentNullException("invitationKey"); - InvitationKey = invitationKey; - } - - /// - /// Serialization constructor - /// - protected GetInvitationByKey() - { - } - - /// - /// Invitation key - /// - public string InvitationKey { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Invitations.Queries +{ + /// + /// Get invitation by using the emailed invitation key + /// + [Message] + public class GetInvitationByKey : Query + { + /// + /// Creates a new instance of . + /// + /// Emailed key + /// invitationKey + public GetInvitationByKey(string invitationKey) + { + if (invitationKey == null) throw new ArgumentNullException("invitationKey"); + InvitationKey = invitationKey; + } + + /// + /// Serialization constructor + /// + protected GetInvitationByKey() + { + } + + /// + /// Invitation key + /// + public string InvitationKey { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Invitations/Queries/GetInvitationByKeyResult.cs b/src/Server/Coderr.Server.Api/Core/Invitations/Queries/GetInvitationByKeyResult.cs similarity index 82% rename from src/Server/OneTrueError.Api/Core/Invitations/Queries/GetInvitationByKeyResult.cs rename to src/Server/Coderr.Server.Api/Core/Invitations/Queries/GetInvitationByKeyResult.cs index 32839128..5f2cd19a 100644 --- a/src/Server/OneTrueError.Api/Core/Invitations/Queries/GetInvitationByKeyResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Invitations/Queries/GetInvitationByKeyResult.cs @@ -1,13 +1,13 @@ -namespace OneTrueError.Api.Core.Invitations.Queries -{ - /// - /// Result for . - /// - public class GetInvitationByKeyResult - { - /// - /// Email address specified when sending the invitation. - /// - public string EmailAddress { get; set; } - } +namespace Coderr.Server.Api.Core.Invitations.Queries +{ + /// + /// Result for . + /// + public class GetInvitationByKeyResult + { + /// + /// Email address specified when sending the invitation. + /// + public string EmailAddress { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Invitations/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Invitations/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Invitations/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Invitations/ReadMe.md diff --git a/src/Server/Coderr.Server.Api/Core/Messaging/Commands/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/NamespaceDoc.cs new file mode 100644 index 00000000..b667f532 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/NamespaceDoc.cs @@ -0,0 +1,18 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Messaging.Commands +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// User related information (such as name, notifcation settings etc.) + /// + /// + /// While accounts are for login authentication and authorization, users are for information about the individual. + /// + /// + [CompilerGenerated] + class NamespaceDoc + { + } +} diff --git a/src/Server/OneTrueError.Api/Core/Messaging/Commands/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Messaging/Commands/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Messaging/Commands/ReadMe.md diff --git a/src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendEmail.cs b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendEmail.cs new file mode 100644 index 00000000..bace4f6f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendEmail.cs @@ -0,0 +1,33 @@ +using System; + +namespace Coderr.Server.Api.Core.Messaging.Commands +{ + /// + /// Send an email. + /// + [Message] + public class SendEmail + { + /// + /// Create a new instance of . + /// + /// Message to send + public SendEmail(EmailMessage message) + { + if (message == null) throw new ArgumentNullException(nameof(message)); + EmailMessage = message; + } + + /// + /// Serialization constructor + /// + protected SendEmail() + { + } + + /// + /// Message to send + /// + public EmailMessage EmailMessage { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Messaging/Commands/SendSms.cs b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendSms.cs similarity index 84% rename from src/Server/OneTrueError.Api/Core/Messaging/Commands/SendSms.cs rename to src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendSms.cs index ced29ace..a6831349 100644 --- a/src/Server/OneTrueError.Api/Core/Messaging/Commands/SendSms.cs +++ b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendSms.cs @@ -1,50 +1,50 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Messaging.Commands -{ - /// - /// Send a cell phone text. - /// - /// - /// - /// Requires a prepaid account at http://onetrueerror.com/services/sms. Add your SMS Api key and the shared - /// secret in your web.config. - /// - /// - /// - /// - /// ]]> - /// - /// - public class SendSms : Command - { - /// - /// Creates a new instance of . - /// - /// - /// E.164 formatted number (]]>, example: +467012345 - /// - /// Message. 160 chars is max for one SMS. - /// phoneNumber; message - public SendSms(string phoneNumber, string message) - { - if (phoneNumber == null) throw new ArgumentNullException("phoneNumber"); - if (message == null) throw new ArgumentNullException("message"); - - PhoneNumber = phoneNumber; - Message = message; - } - - /// - /// Message. 160 chars is max for one SMS. - /// - public string Message { get; set; } - - /// - /// E.164 formatted number (]]>, example: +467012345 - /// - public string PhoneNumber { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Messaging.Commands +{ + /// + /// Send a cell phone text. + /// + /// + /// + /// Requires a prepaid account at http://coderr.io/services/sms. Add your SMS Api key and the shared + /// secret in your web.config. + /// + /// + /// + /// + /// ]]> + /// + /// + [Message] + public class SendSms + { + /// + /// Creates a new instance of . + /// + /// + /// E.164 formatted number (]]>, example: +467012345 + /// + /// Message. 160 chars is max for one SMS. + /// phoneNumber; message + public SendSms(string phoneNumber, string message) + { + if (phoneNumber == null) throw new ArgumentNullException("phoneNumber"); + if (message == null) throw new ArgumentNullException("message"); + + PhoneNumber = phoneNumber; + Message = message; + } + + /// + /// Message. 160 chars is max for one SMS. + /// + public string Message { get; set; } + + /// + /// E.164 formatted number (]]>, example: +467012345 + /// + public string PhoneNumber { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Messaging/Commands/SendTemplateEmail.cs b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendTemplateEmail.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Messaging/Commands/SendTemplateEmail.cs rename to src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendTemplateEmail.cs index e7fe0788..59f26275 100644 --- a/src/Server/OneTrueError.Api/Core/Messaging/Commands/SendTemplateEmail.cs +++ b/src/Server/Coderr.Server.Api/Core/Messaging/Commands/SendTemplateEmail.cs @@ -1,66 +1,66 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Messaging.Commands -{ - /// - /// Send email using a template. - /// - public class SendTemplateEmail : Command - { - /// - /// Creates a new instance of . - /// - /// Mail title (i.e. not the subject) - /// - /// Template to load (should be a sub folder to the invoking class, look at - /// RequestPasswordResetHandler for an example. - /// - /// - public SendTemplateEmail(string mailTitle, string templateName) - { - if (mailTitle == null) throw new ArgumentNullException("mailTitle"); - if (templateName == null) throw new ArgumentNullException("templateName"); - - MailTitle = mailTitle; - TemplateName = templateName; - } - - /// - /// Serialization constructor - /// - protected SendTemplateEmail() - { - } - - /// - /// Mail title (in the layout template) - /// - public string MailTitle { get; set; } - - /// - /// View model - /// - public object Model { get; set; } - - /// - /// Resources to use in the template - /// - public EmailResource[] Resources { get; set; } - - /// - /// Mail subject. - /// - public string Subject { get; set; } - - /// - /// Name of the template to parse - /// - public string TemplateName { get; private set; } - - /// - /// Whom to send to (TODO: is accountId OK?) - /// - public string To { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Messaging.Commands +{ + /// + /// Send email using a template. + /// + [Message] + public class SendTemplateEmail + { + /// + /// Creates a new instance of . + /// + /// Mail title (i.e. not the subject) + /// + /// Template to load (should be a sub folder to the invoking class, look at + /// RequestPasswordResetHandler for an example. + /// + /// + public SendTemplateEmail(string mailTitle, string templateName) + { + if (mailTitle == null) throw new ArgumentNullException("mailTitle"); + if (templateName == null) throw new ArgumentNullException("templateName"); + + MailTitle = mailTitle; + TemplateName = templateName; + } + + /// + /// Serialization constructor + /// + protected SendTemplateEmail() + { + } + + /// + /// Mail title (in the layout template) + /// + public string MailTitle { get; set; } + + /// + /// View model + /// + public object Model { get; set; } + + /// + /// Resources to use in the template + /// + public EmailResource[] Resources { get; set; } + + /// + /// Mail subject. + /// + public string Subject { get; set; } + + /// + /// Name of the template to parse + /// + public string TemplateName { get; private set; } + + /// + /// Whom to send to (TODO: is accountId OK?) + /// + public string To { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Messaging/EmailAddress.cs b/src/Server/Coderr.Server.Api/Core/Messaging/EmailAddress.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/Messaging/EmailAddress.cs rename to src/Server/Coderr.Server.Api/Core/Messaging/EmailAddress.cs index d33877bd..e8a4ef96 100644 --- a/src/Server/OneTrueError.Api/Core/Messaging/EmailAddress.cs +++ b/src/Server/Coderr.Server.Api/Core/Messaging/EmailAddress.cs @@ -1,44 +1,44 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Api.Core.Messaging -{ - /// - /// Email address - /// - public class EmailAddress - { - /// - /// Creates a new instance of . - /// - /// email address or account id - public EmailAddress(string address) - { - if (address == null) throw new ArgumentNullException("address"); - - var attr = new EmailAddressAttribute(); - int accountId; - if (!attr.IsValid(address) && !int.TryParse(address, out accountId)) - throw new FormatException("'" + address + "' is not a valid email or account id."); - - Address = address; - } - - /// - /// Serialization constructor - /// - protected EmailAddress() - { - } - - /// - /// Email address or AccountId. - /// - public string Address { get; set; } - - /// - /// Recipient name - /// - public string Name { get; set; } - } +using System; +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.Api.Core.Messaging +{ + /// + /// Email address + /// + public class EmailAddress + { + /// + /// Creates a new instance of . + /// + /// email address or account id + public EmailAddress(string address) + { + if (address == null) throw new ArgumentNullException("address"); + + var attr = new EmailAddressAttribute(); + int accountId; + if (!attr.IsValid(address) && !int.TryParse(address, out accountId)) + throw new FormatException("'" + address + "' is not a valid email or account id."); + + Address = address; + } + + /// + /// Serialization constructor + /// + protected EmailAddress() + { + } + + /// + /// Email address or AccountId. + /// + public string Address { get; set; } + + /// + /// Recipient name + /// + public string Name { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Messaging/EmailMessage.cs b/src/Server/Coderr.Server.Api/Core/Messaging/EmailMessage.cs similarity index 92% rename from src/Server/OneTrueError.Api/Core/Messaging/EmailMessage.cs rename to src/Server/Coderr.Server.Api/Core/Messaging/EmailMessage.cs index d0bcfc90..e69a6ef3 100644 --- a/src/Server/OneTrueError.Api/Core/Messaging/EmailMessage.cs +++ b/src/Server/Coderr.Server.Api/Core/Messaging/EmailMessage.cs @@ -1,74 +1,79 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace OneTrueError.Api.Core.Messaging -{ - /// - /// Used to send emails. - /// - /// - /// - /// Used instead of the .NET classes to allow third party email services. - /// - /// - public class EmailMessage - { - /// - /// Create a new instance of - /// - public EmailMessage() - { - Resources = new List(); - } - - /// - /// Create a new instance of - /// - /// Destination - public EmailMessage(string recipient) - { - if (recipient == null) throw new ArgumentNullException("recipient"); - Recipients = new[] {new EmailAddress(recipient)}; - Resources = new List(); - } - - /// - /// Create a new instance of - /// - /// List of recipients - public EmailMessage(IReadOnlyList recipients) - { - if (recipients == null) throw new ArgumentNullException("recipients"); - if (recipients.Count == 0) throw new ArgumentException("Tried to send to an empty list.", "recipients"); - - Recipients = recipients.Select(x => new EmailAddress(x)).ToArray(); - Resources = new List(); - } - - /// - /// Body (should be send as HTML) - /// - public string HtmlBody { get; set; } - - /// - /// List of recipients - /// - public EmailAddress[] Recipients { get; set; } - - /// - /// Attachments and/or inline images. - /// - public IList Resources { get; set; } - - /// - /// Subject line - /// - public string Subject { get; set; } - - /// - /// Text body - /// - public string TextBody { get; set; } - } +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Coderr.Server.Api.Core.Messaging +{ + /// + /// Used to send emails. + /// + /// + /// + /// Used instead of the .NET classes to allow third party email services. + /// + /// + public class EmailMessage + { + /// + /// Create a new instance of + /// + public EmailMessage() + { + Resources = new List(); + } + + /// + /// Create a new instance of + /// + /// Destination + public EmailMessage(string recipient) + { + if (recipient == null) throw new ArgumentNullException("recipient"); + Recipients = new[] {new EmailAddress(recipient)}; + Resources = new List(); + } + + /// + /// Create a new instance of + /// + /// List of recipients + public EmailMessage(IReadOnlyList recipients) + { + if (recipients == null) throw new ArgumentNullException("recipients"); + if (recipients.Count == 0) throw new ArgumentException("Tried to send to an empty list.", "recipients"); + + Recipients = recipients.Select(x => new EmailAddress(x)).ToArray(); + Resources = new List(); + } + + /// + /// Body (should be send as HTML) + /// + public string HtmlBody { get; set; } + + /// + /// List of recipients + /// + public EmailAddress[] Recipients { get; set; } + + /// + /// Whom should replies be sent to. + /// + public EmailAddress ReplyTo { get; set; } + + /// + /// Attachments and/or inline images. + /// + public IList Resources { get; set; } + + /// + /// Subject line + /// + public string Subject { get; set; } + + /// + /// Text body + /// + public string TextBody { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Messaging/EmailResource.cs b/src/Server/Coderr.Server.Api/Core/Messaging/EmailResource.cs similarity index 84% rename from src/Server/OneTrueError.Api/Core/Messaging/EmailResource.cs rename to src/Server/Coderr.Server.Api/Core/Messaging/EmailResource.cs index ea778d1e..7b72dee7 100644 --- a/src/Server/OneTrueError.Api/Core/Messaging/EmailResource.cs +++ b/src/Server/Coderr.Server.Api/Core/Messaging/EmailResource.cs @@ -1,41 +1,41 @@ -using System; -using System.IO; - -namespace OneTrueError.Api.Core.Messaging -{ - /// - /// A resource attached to an email (typically an image) - /// - public class EmailResource - { - /// - /// Name of the resource (refered to using a cid in the email body) - /// - /// CID - /// Actual content - public EmailResource(string name, Stream content) - { - if (name == null) throw new ArgumentNullException("name"); - if (content == null) throw new ArgumentNullException("content"); - Name = name; - Content = content; - } - - /// - /// Serialization constructor - /// - protected EmailResource() - { - } - - /// - /// Contents of the resource. Stream must be readable. - /// - public Stream Content { get; set; } - - /// - /// CID - /// - public string Name { get; set; } - } +using System; +using System.IO; + +namespace Coderr.Server.Api.Core.Messaging +{ + /// + /// A resource attached to an email (typically an image) + /// + public class EmailResource + { + /// + /// Name of the resource (refered to using a cid in the email body) + /// + /// CID + /// Actual content + public EmailResource(string name, byte[] content) + { + if (name == null) throw new ArgumentNullException("name"); + if (content == null) throw new ArgumentNullException("content"); + Name = name; + Content = content; + } + + /// + /// Serialization constructor + /// + protected EmailResource() + { + } + + /// + /// Contents of the resource. Stream must be readable. + /// + public byte[] Content { get; set; } + + /// + /// CID + /// + public string Name { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Messaging/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Messaging/NamespaceDoc.cs new file mode 100644 index 00000000..a5c28721 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Messaging/NamespaceDoc.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Messaging +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// Messaging, which includes sending emails. + /// + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Messaging/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Messaging/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Messaging/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Messaging/ReadMe.md diff --git a/src/Server/Coderr.Server.Api/Core/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/NamespaceDoc.cs new file mode 100644 index 00000000..6906fff7 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/NamespaceDoc.cs @@ -0,0 +1,17 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// Core + /// + /// + /// Core contains all basic functionality to get Coderr running. A minimal set of analysis is done here. + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Notifications/AddNotification.cs b/src/Server/Coderr.Server.Api/Core/Notifications/AddNotification.cs new file mode 100644 index 00000000..0a2bbc8b --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Notifications/AddNotification.cs @@ -0,0 +1,99 @@ +using System; + +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local + +namespace Coderr.Server.Api.Core.Notifications +{ + /// + /// Add a user notification + /// + /// + /// + /// User notifications are typically used when the user need to do some action (typically due to configuration + /// issues). + /// + /// + [Message] + public class AddNotification + { + /// + /// Creates a new instance of . + /// + /// user account id + /// message to display + public AddNotification(int accountId, string message) + { + if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); + + Message = message ?? throw new ArgumentNullException("message"); + AccountId = accountId; + } + + /// + /// Creates a new instance of . + /// + /// Send this message to first user that logs in with the specified role + /// Message to display to the user + public AddNotification(string roleName, string message) + { + RoleName = roleName ?? throw new ArgumentNullException("roleName"); + Message = message ?? throw new ArgumentNullException("message"); + } + + /// + /// Serialization constructor + /// + protected AddNotification() + { + + } + + /// + /// Display only for the specified user. + /// + public int? AccountId { get; private set; } + + + /// + /// Amount of time to wait until creating this notification again once the user have read the notification. + /// + /// + /// + /// Requires to be set. + /// + /// + public TimeSpan? HoldbackInterval { get; set; } + + /// + /// Message to display to user + /// + public string Message { get; private set; } + + /// + /// There may only exist one notification of each type for the target user(s). + /// + /// + /// + /// Set this to a unique value for your module if you want to prevent multiple instances of the same notification + /// to be created. Useful for instance if you sent a configuration failure message when a new report is created. + /// Without this type, the same notification would be created every time a report arrives until the configuration + /// have been corrected. + /// + /// + /// The notification will be sent again when the user have read it, unless you also have set the hold-back + /// timespan. + /// + /// + public string NotificationType { get; set; } + + /// + /// Display this message for everyone with the given role + /// + /// + /// + /// Alternative to . + /// + /// + public string RoleName { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/ReadMe.md b/src/Server/Coderr.Server.Api/Core/ReadMe.md new file mode 100644 index 00000000..c307b3d5 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/ReadMe.md @@ -0,0 +1,4 @@ +Core +============ + +Core contains all basic functionality to get codeRR running. A minimal set of analysis is done here. diff --git a/src/Server/Coderr.Server.Api/Core/Reports/ContextCollectionDTO.cs b/src/Server/Coderr.Server.Api/Core/Reports/ContextCollectionDTO.cs new file mode 100644 index 00000000..38eed818 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Reports/ContextCollectionDTO.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Coderr.Server.Api.Core.Reports +{ + /// + /// Context collection DTO. + /// + public class ContextCollectionDTO + { + /// + /// Creates a new instance of . + /// + protected ContextCollectionDTO() + { + } + + /// + /// Creates a new instance of . + /// + /// Name as specified in the client library + /// Properties. + public ContextCollectionDTO(string name, IDictionary items) + { + if (name == null) throw new ArgumentNullException("name"); + if (items == null) throw new ArgumentNullException("items"); + + Name = name; + Properties = items; + } + + + /// + /// Name as specified in the client library + /// + public string Name { get; set; } + + /// + /// Properties. + /// + public IDictionary Properties { get; set; } + + /// + /// Returns a string that represents the current object. + /// + /// + /// A string that represents the current object. + /// + /// 2 + public override string ToString() + { + var flatten = Properties.Select(x => x.Key + "=" + x.Value); + var joinProps = string.Join(", ", flatten); + return string.Format("{0} [{1}]", Name, joinProps); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Reports/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Reports/NamespaceDoc.cs new file mode 100644 index 00000000..0b119288 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Reports/NamespaceDoc.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Reports +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// Reports represent the recieved exception along with all collected context information. + /// + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReport.cs b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReport.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Reports/Queries/GetReport.cs rename to src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReport.cs index a26ef7c4..9558299b 100644 --- a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReport.cs +++ b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReport.cs @@ -1,34 +1,35 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Reports.Queries -{ - /// - /// Get report (i.e. exception and context collections) - /// - public class GetReport : Query - { - /// - /// Serialization constructor. - /// - protected GetReport() - { - } - - /// - /// Creates a new instance of . - /// - /// report - /// reportId - public GetReport(int reportId) - { - if (reportId <= 0) throw new ArgumentOutOfRangeException("reportId"); - ReportId = reportId; - } - - /// - /// Report id - /// - public int ReportId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Reports.Queries +{ + /// + /// Get report (i.e. exception and context collections) + /// + [Message] + public class GetReport : Query + { + /// + /// Serialization constructor. + /// + protected GetReport() + { + } + + /// + /// Creates a new instance of . + /// + /// report + /// reportId + public GetReport(int reportId) + { + if (reportId <= 0) throw new ArgumentOutOfRangeException("reportId"); + ReportId = reportId; + } + + /// + /// Report id + /// + public int ReportId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportException.cs b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportException.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportException.cs rename to src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportException.cs index c93b2190..ffd29637 100644 --- a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportException.cs +++ b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportException.cs @@ -1,53 +1,53 @@ -namespace OneTrueError.Api.Core.Reports.Queries -{ - /// - /// Partial result for . - /// - public class GetReportException - { - /// - /// Assembly that the exception type is declared in. - /// - public string AssemblyName { get; set; } - - /// - /// Base class names (without namespace). At least "Exception". - /// - public string[] BaseClasses { get; set; } - - /// - /// Typically exception.ToString() - /// - public string Everything { get; set; } - - /// - /// Full type name - /// - public string FullName { get; set; } - - /// - /// Inner exception (or null) - /// - public GetReportException InnerException { get; set; } - - /// - /// Exception.Message - /// - public string Message { get; set; } - - /// - /// Exception name (first line of the exception message) - /// - public string Name { get; set; } - - /// - /// Type namespace - /// - public string Namespace { get; set; } - - /// - /// Stack trace. - /// - public string StackTrace { get; set; } - } +namespace Coderr.Server.Api.Core.Reports.Queries +{ + /// + /// Partial result for . + /// + public class GetReportException + { + /// + /// Assembly that the exception type is declared in. + /// + public string AssemblyName { get; set; } + + /// + /// Base class names (without namespace). At least "Exception". + /// + public string[] BaseClasses { get; set; } + + /// + /// Typically exception.ToString() + /// + public string Everything { get; set; } + + /// + /// Full type name + /// + public string FullName { get; set; } + + /// + /// Inner exception (or null) + /// + public GetReportException InnerException { get; set; } + + /// + /// Exception.Message + /// + public string Message { get; set; } + + /// + /// Exception name (first line of the exception message) + /// + public string Name { get; set; } + + /// + /// Type namespace + /// + public string Namespace { get; set; } + + /// + /// Stack trace. + /// + public string StackTrace { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportList.cs b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportList.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportList.cs rename to src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportList.cs index 1f6f401f..79e5beec 100644 --- a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportList.cs +++ b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportList.cs @@ -1,44 +1,45 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Reports.Queries -{ - /// - /// Get reports - /// - public class GetReportList : Query - { - /// - /// Get reports - /// - /// incident to get reports for - public GetReportList(int incidentId) - { - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - } - - /// - /// Serialization constructor. - /// - protected GetReportList() - { - } - - - /// - /// Incident id. - /// - public int IncidentId { get; private set; } - - /// - /// Page number (one based index) - /// - public int PageNumber { get; set; } - - /// - /// Page size (default is 20). - /// - public int PageSize { get; set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Reports.Queries +{ + /// + /// Get reports + /// + [Message] + public class GetReportList : Query + { + /// + /// Get reports + /// + /// incident to get reports for + public GetReportList(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + } + + /// + /// Serialization constructor. + /// + protected GetReportList() + { + } + + + /// + /// Incident id. + /// + public int IncidentId { get; private set; } + + /// + /// Page number (one based index) + /// + public int PageNumber { get; set; } + + /// + /// Page size (default is 20). + /// + public int PageSize { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportListResult.cs b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportListResult.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportListResult.cs rename to src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportListResult.cs index 68a0aedb..59b2bfd5 100644 --- a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportListResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportListResult.cs @@ -1,48 +1,48 @@ -using System; - -namespace OneTrueError.Api.Core.Reports.Queries -{ - /// - /// Result for . - /// - public class GetReportListResult - { - /// - /// Creates a new instance of . - /// - /// Result items - /// items - public GetReportListResult(GetReportListResultItem[] items) - { - if (items == null) throw new ArgumentNullException("items"); - Items = items; - } - - /// - /// Serialization constructor. - /// - protected GetReportListResult() - { - } - - /// - /// Items on this page. - /// - public GetReportListResultItem[] Items { get; set; } - - /// - /// Page number being returned - /// - public int PageNumber { get; set; } - - /// - /// Number of items on this page - /// - public int PageSize { get; set; } - - /// - /// Total number of items that a non-paged query would return - /// - public int TotalCount { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Reports.Queries +{ + /// + /// Result for . + /// + public class GetReportListResult + { + /// + /// Creates a new instance of . + /// + /// Result items + /// items + public GetReportListResult(GetReportListResultItem[] items) + { + if (items == null) throw new ArgumentNullException("items"); + Items = items; + } + + /// + /// Serialization constructor. + /// + protected GetReportListResult() + { + } + + /// + /// Items on this page. + /// + public GetReportListResultItem[] Items { get; set; } + + /// + /// Page number being returned + /// + public int PageNumber { get; set; } + + /// + /// Number of items on this page + /// + public int PageSize { get; set; } + + /// + /// Total number of items that a non-paged query would return + /// + public int TotalCount { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportListResultItem.cs b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportListResultItem.cs similarity index 90% rename from src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportListResultItem.cs rename to src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportListResultItem.cs index 7cc0b54d..0e07a0a4 100644 --- a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportListResultItem.cs +++ b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportListResultItem.cs @@ -1,30 +1,30 @@ -using System; - -namespace OneTrueError.Api.Core.Reports.Queries -{ - /// - /// Item for . - /// - public class GetReportListResultItem - { - /// - /// When the report was created in the client library - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// Report id - /// - public int Id { get; set; } - - /// - /// Exception message. - /// - public string Message { get; set; } - - /// - /// IP that uploaded the report. - /// - public string RemoteAddress { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Reports.Queries +{ + /// + /// Item for . + /// + public class GetReportListResultItem + { + /// + /// When the report was created in the client library + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// Report id + /// + public int Id { get; set; } + + /// + /// Exception message. + /// + public string Message { get; set; } + + /// + /// IP that uploaded the report. + /// + public string RemoteAddress { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportResult.cs b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportResult.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportResult.cs rename to src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportResult.cs index 993510f9..a72ddd41 100644 --- a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportResult.cs +++ b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportResult.cs @@ -1,60 +1,60 @@ -using System; - -namespace OneTrueError.Api.Core.Reports.Queries -{ - /// - /// Result for . - /// - public class GetReportResult - { - /// - /// Context collections - /// - public GetReportResultContextCollection[] ContextCollections { get; set; } - - /// - /// When the report was created in the client library - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// Email address (if the user would like to get status updates). - /// - public string EmailAddress { get; set; } - - /// - /// Unique id generated in the client library - /// - public string ErrorId { get; set; } - - /// - /// Actual exception - /// - public GetReportException Exception { get; set; } - - /// - /// Report id - /// - public string Id { get; set; } - - /// - /// Incident that this report belongs to. - /// - public string IncidentId { get; set; } - - /// - /// First line from the exception message. - /// - public string Message { get; set; } - - /// - /// Stack trace - /// - public string StackTrace { get; set; } - - /// - /// Error description written by the user (if any). - /// - public string UserFeedback { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Reports.Queries +{ + /// + /// Result for . + /// + public class GetReportResult + { + /// + /// Context collections + /// + public GetReportResultContextCollection[] ContextCollections { get; set; } + + /// + /// When the report was created in the client library + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// Email address (if the user would like to get status updates). + /// + public string EmailAddress { get; set; } + + /// + /// Unique id generated in the client library + /// + public string ErrorId { get; set; } + + /// + /// Actual exception + /// + public GetReportException Exception { get; set; } + + /// + /// Report id + /// + public string Id { get; set; } + + /// + /// Incident that this report belongs to. + /// + public string IncidentId { get; set; } + + /// + /// First line from the exception message. + /// + public string Message { get; set; } + + /// + /// Stack trace + /// + public string StackTrace { get; set; } + + /// + /// Error description written by the user (if any). + /// + public string UserFeedback { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportResultContextCollection.cs b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportResultContextCollection.cs similarity index 93% rename from src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportResultContextCollection.cs rename to src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportResultContextCollection.cs index fd06bb94..05b10c7e 100644 --- a/src/Server/OneTrueError.Api/Core/Reports/Queries/GetReportResultContextCollection.cs +++ b/src/Server/Coderr.Server.Api/Core/Reports/Queries/GetReportResultContextCollection.cs @@ -1,42 +1,42 @@ -using System; - -namespace OneTrueError.Api.Core.Reports.Queries -{ - /// - /// Context collection for . - /// - public class GetReportResultContextCollection - { - /// - /// Creates a new instance of . - /// - /// collection name - /// all uploaded properties - /// name; properties - public GetReportResultContextCollection(string name, KeyValuePair[] properties) - { - if (name == null) throw new ArgumentNullException("name"); - if (properties == null) throw new ArgumentNullException("properties"); - - Name = name; - Properties = properties; - } - - /// - /// Serialization constructor. - /// - protected GetReportResultContextCollection() - { - } - - /// - /// Context collection name - /// - public string Name { get; set; } - - /// - /// Properties. - /// - public KeyValuePair[] Properties { get; set; } - } +using System; + +namespace Coderr.Server.Api.Core.Reports.Queries +{ + /// + /// Context collection for . + /// + public class GetReportResultContextCollection + { + /// + /// Creates a new instance of . + /// + /// collection name + /// all uploaded properties + /// name; properties + public GetReportResultContextCollection(string name, KeyValuePair[] properties) + { + if (name == null) throw new ArgumentNullException("name"); + if (properties == null) throw new ArgumentNullException("properties"); + + Name = name; + Properties = properties; + } + + /// + /// Serialization constructor. + /// + protected GetReportResultContextCollection() + { + } + + /// + /// Context collection name + /// + public string Name { get; set; } + + /// + /// Properties. + /// + public KeyValuePair[] Properties { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/Queries/KeyValuePair.cs b/src/Server/Coderr.Server.Api/Core/Reports/Queries/KeyValuePair.cs similarity index 91% rename from src/Server/OneTrueError.Api/Core/Reports/Queries/KeyValuePair.cs rename to src/Server/Coderr.Server.Api/Core/Reports/Queries/KeyValuePair.cs index 208a8c6a..f27dbd03 100644 --- a/src/Server/OneTrueError.Api/Core/Reports/Queries/KeyValuePair.cs +++ b/src/Server/Coderr.Server.Api/Core/Reports/Queries/KeyValuePair.cs @@ -1,41 +1,41 @@ -using System; - -namespace OneTrueError.Api.Core.Reports.Queries -{ - /// - /// Key value pair - /// - public class KeyValuePair - { - /// - /// Creates a new instance of . - /// - /// key - /// value (null is allowed) - /// key; value - public KeyValuePair(string key, string value) - { - if (key == null) throw new ArgumentNullException("key"); - - Key = key; - Value = value; - } - - /// - /// Serialization constructor - /// - protected KeyValuePair() - { - } - - /// - /// Key - /// - public string Key { get; private set; } - - /// - /// Value - /// - public string Value { get; private set; } - } +using System; + +namespace Coderr.Server.Api.Core.Reports.Queries +{ + /// + /// Key value pair + /// + public class KeyValuePair + { + /// + /// Creates a new instance of . + /// + /// key + /// value (null is allowed) + /// key; value + public KeyValuePair(string key, string value) + { + if (key == null) throw new ArgumentNullException("key"); + + Key = key; + Value = value; + } + + /// + /// Serialization constructor + /// + protected KeyValuePair() + { + } + + /// + /// Key + /// + public string Key { get; private set; } + + /// + /// Value + /// + public string Value { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Reports/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Reports/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Reports/ReadMe.md diff --git a/src/Server/Coderr.Server.Api/Core/Reports/ReportDTO.cs b/src/Server/Coderr.Server.Api/Core/Reports/ReportDTO.cs new file mode 100644 index 00000000..175b1f3e --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Reports/ReportDTO.cs @@ -0,0 +1,55 @@ +using System; + +namespace Coderr.Server.Api.Core.Reports +{ + /// + /// Report representation. + /// + public class ReportDTO + { + /// + /// Application that the incident and report belongs in. + /// + public int ApplicationId { get; set; } + + /// + /// A collection of context information such as HTTP request information or computer hardware info. + /// + public ContextCollectionDTO[] ContextCollections { get; set; } + + /// + /// Date specified at client side + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// Exception which was caught. + /// + public ReportExeptionDTO Exception { get; set; } + + /// + /// DB primary key + /// + public int Id { get; set; } + + /// + /// DB primary key + /// + public int IncidentId { get; set; } + + /// + /// Ip of the report uploader. + /// + public string RemoteAddress { get; set; } + + /// + /// Gets error id (unique identifier used in communication with the customer to identify this error) + /// + public string ReportId { get; set; } + + /// + /// Version of the report + /// + public string ReportVersion { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Reports/ReportExeptionDTO.cs b/src/Server/Coderr.Server.Api/Core/Reports/ReportExeptionDTO.cs new file mode 100644 index 00000000..b35c407a --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Reports/ReportExeptionDTO.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Core.Reports +{ + /// + /// Model used to wrap all information from an exception. + /// + public class ReportExeptionDTO + { + /// + /// Initializes a new instance of the class. + /// + public ReportExeptionDTO() + { + Properties = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Assembly name (version included) + /// + public string AssemblyName { get; set; } + + /// + /// Exception base classes. Most specific first: ArgumentOutOfRangeException, ArgumentException, + /// Exception. + /// + public string[] BaseClasses { get; set; } + + /// + /// Everything (exception.ToString()) + /// + public string Everything { get; set; } + + /// + /// Full type name (namespace + class name) + /// + public string FullName { get; set; } + + /// + /// Inner exception (if any; otherwise null). + /// + public ReportExeptionDTO InnerException { get; set; } + + /// + /// Exception message + /// + public string Message { get; set; } + + /// + /// Type name + /// + public string Name { get; set; } + + /// + /// Namespace that the exception is in + /// + public string Namespace { get; set; } + + /// + /// All properties (public and private) + /// + public IDictionary Properties { get; set; } + + /// + /// Stack trace, line numbers included if your app also distributes the PDB files. + /// + public string StackTrace { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Reports/ReportExtensions.cs b/src/Server/Coderr.Server.Api/Core/Reports/ReportExtensions.cs new file mode 100644 index 00000000..3a4bea7c --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Reports/ReportExtensions.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Coderr.Server.Api.Core.Reports +{ + public static class ReportExtensions + { + + public static ContextCollectionDTO GetCoderrCollection( + this IEnumerable instance) + { + return instance.FirstOrDefault(x => x.Name == "CoderrData"); + } + + public static ContextCollectionDTO GetCoderrCollection( + this ReportDTO instance) + { + return instance.ContextCollections.FirstOrDefault(x => x.Name == "CoderrData"); + } + + + + } + +} diff --git a/src/Server/Coderr.Server.Api/Core/Settings/Commands/SaveAccountSetting.cs b/src/Server/Coderr.Server.Api/Core/Settings/Commands/SaveAccountSetting.cs new file mode 100644 index 00000000..1f842c84 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Settings/Commands/SaveAccountSetting.cs @@ -0,0 +1,11 @@ +namespace Coderr.Server.Api.Core.Settings.Commands +{ + [Command] + public class SaveAccountSetting + { + public int AccountId { get; set; } + public string Name { get; set; } + public string Value { get; set; } + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Settings/Commands/SaveAccountSettings.cs b/src/Server/Coderr.Server.Api/Core/Settings/Commands/SaveAccountSettings.cs new file mode 100644 index 00000000..04af3421 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Settings/Commands/SaveAccountSettings.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Api.Core.Settings.Commands +{ + [Command] + public class SaveAccountSettings + { + public int AccountId { get; set; } + public IDictionary Settings { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSetting.cs b/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSetting.cs new file mode 100644 index 00000000..07dc0479 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSetting.cs @@ -0,0 +1,11 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Settings.Queries +{ + [Message] + public class GetAccountSetting : Query + { + public int AccountId { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettingResult.cs b/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettingResult.cs new file mode 100644 index 00000000..78c767b6 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettingResult.cs @@ -0,0 +1,7 @@ +namespace Coderr.Server.Api.Core.Settings.Queries +{ + public class GetAccountSettingResult + { + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettings.cs b/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettings.cs new file mode 100644 index 00000000..e6fe412c --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettings.cs @@ -0,0 +1,10 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Settings.Queries +{ + [Message] + public class GetAccountSettings : Query + { + public int AccountId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettingsResult.cs b/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettingsResult.cs new file mode 100644 index 00000000..8acc42c9 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Settings/Queries/GetAccountSettingsResult.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Api.Core.Settings.Queries +{ + public class GetAccountSettingsResult + { + public IDictionary Settings { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Support/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Support/NamespaceDoc.cs new file mode 100644 index 00000000..c43a865e --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Support/NamespaceDoc.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Support +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// Used to get support from Gauffin Interactive AB + /// + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Support/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Support/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Support/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Support/ReadMe.md diff --git a/src/Server/Coderr.Server.Api/Core/Support/SendSupportRequest.cs b/src/Server/Coderr.Server.Api/Core/Support/SendSupportRequest.cs new file mode 100644 index 00000000..e1709881 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Support/SendSupportRequest.cs @@ -0,0 +1,24 @@ +namespace Coderr.Server.Api.Core.Support +{ + /// + /// Send a support request to 1TCompany AB + /// + [Message] + public class SendSupportRequest + { + /// + /// Problem statement + /// + public string Message { get; set; } + + /// + /// Why do we want support, huh? + /// + public string Subject { get; set; } + + /// + /// Url of the page that did not work + /// + public string Url { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Users/Commands/DeleteBrowserSubscription.cs b/src/Server/Coderr.Server.Api/Core/Users/Commands/DeleteBrowserSubscription.cs new file mode 100644 index 00000000..bed2f178 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Users/Commands/DeleteBrowserSubscription.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.Api.Core.Users.Commands +{ + [Message] + public class DeleteBrowserSubscription + { + public int UserId { get; set; } + public string Endpoint { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Users/Commands/StoreBrowserSubscription.cs b/src/Server/Coderr.Server.Api/Core/Users/Commands/StoreBrowserSubscription.cs new file mode 100644 index 00000000..88d41326 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Users/Commands/StoreBrowserSubscription.cs @@ -0,0 +1,17 @@ +namespace Coderr.Server.Api.Core.Users.Commands +{ + /// + /// https://tools.ietf.org/html/draft-ietf-webpush-encryption-08 + /// + [Message] + public class StoreBrowserSubscription + { + public int UserId { get; set; } + public string Endpoint { get; set; } + + public string PublicKey { get; set; } + public string AuthenticationSecret { get; set; } + + public long? ExpirationTime { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Core/Users/Commands/UpdateNotifications.cs b/src/Server/Coderr.Server.Api/Core/Users/Commands/UpdateNotifications.cs new file mode 100644 index 00000000..0ad0a4ea --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Users/Commands/UpdateNotifications.cs @@ -0,0 +1,50 @@ +namespace Coderr.Server.Api.Core.Users.Commands +{ + /// + /// Update user notifications + /// + [Message] + public class UpdateNotifications + { + /// + /// Application that the settings is for (0 = general settings) + /// + public int ApplicationId { get; set; } + + /// + /// How to notify when a new incident is created (received an unique exception) + /// + public NotificationState NotifyOnNewIncidents { get; set; } + + /// + /// How to notify when an incident is updated to critical. + /// + public NotificationState NotifyOnCriticalIncidents { get; set; } + + /// + /// How to notify when an incident is updated to important. + /// + public NotificationState NotifyOnImportantIncidents { get; set; } + + /// + /// How to notify user when a peak is detected + /// + public NotificationState NotifyOnPeaks { get; set; } + + /// + /// How to notify when we receive a new report on a closed incident. + /// + public NotificationState NotifyOnReOpenedIncident { get; set; } + + /// + /// How to notify when an user have written an error description + /// + public NotificationState NotifyOnUserFeedback { get; set; } + + + /// + /// User that configured its settings. + /// + public int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Users/Commands/UpdatePersonalSettings.cs b/src/Server/Coderr.Server.Api/Core/Users/Commands/UpdatePersonalSettings.cs new file mode 100644 index 00000000..2d51c4fc --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Users/Commands/UpdatePersonalSettings.cs @@ -0,0 +1,41 @@ +namespace Coderr.Server.Api.Core.Users.Commands +{ + /// + /// Update personal settings. + /// + [Message] + public class UpdatePersonalSettings + { + /// + /// Change email address + /// + /// + /// + /// Do not required additional verification, we trust the user once it has an activated account. + /// + /// + public string EmailAddress { get; set; } + + /// + /// First name (if specified) + /// + public string FirstName { get; set; } + + + /// + /// Last name (if specified) + /// + public string LastName { get; set; } + + /// + /// Mobile number (E.164 formatted) + /// + public string MobileNumber { get; set; } + + /// + /// Account that the settings are for + /// + [IgnoreField] + public int UserId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Users/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/Core/Users/NamespaceDoc.cs new file mode 100644 index 00000000..475f4916 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Users/NamespaceDoc.cs @@ -0,0 +1,20 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api.Core.Users +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// User related information (such as name, notifcation settings etc.) + /// + /// + /// + /// While accounts are for login authentication and authorization, users are for information about the + /// individual. + /// + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Users/NotificationSettings.cs b/src/Server/Coderr.Server.Api/Core/Users/NotificationSettings.cs new file mode 100644 index 00000000..757c318d --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Users/NotificationSettings.cs @@ -0,0 +1,40 @@ +using Coderr.Server.Api.Core.Users.Queries; + +namespace Coderr.Server.Api.Core.Users +{ + /// + /// Notification settings for . + /// + public class NotificationSettings + { + /// + /// How to notify when an incident is updated to critical. + /// + public NotificationState NotifyOnCriticalIncidents { get; set; } + + /// + /// How to notify when an incident is updated to important. + /// + public NotificationState NotifyOnImportantIncidents { get; set; } + + /// + /// How to notify when a new incident is created (received an unique exception) + /// + public NotificationState NotifyOnNewIncidents { get; set; } + + /// + /// How to notify user when a peak is detected + /// + public NotificationState NotifyOnPeaks { get; set; } + + /// + /// How to notify when we receive a new report on a closed incident. + /// + public NotificationState NotifyOnReOpenedIncident { get; set; } + + /// + /// How to notify when an user have written an error description + /// + public NotificationState NotifyOnUserFeedback { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Users/NotificationState.cs b/src/Server/Coderr.Server.Api/Core/Users/NotificationState.cs new file mode 100644 index 00000000..191af889 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Users/NotificationState.cs @@ -0,0 +1,33 @@ +namespace Coderr.Server.Api.Core.Users +{ + /// + /// Type of notification to use + /// + public enum NotificationState + { + /// + /// Use global setting + /// + UseGlobalSetting = 1, + + /// + /// Do not notify + /// + Disabled = 2, + + /// + /// By cellphone (text message) + /// + Cellphone = 3, + + /// + /// By email + /// + Email = 4, + + /// + /// Use browser/desktop notifications. + /// + BrowserNotification = 5 + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Users/Queries/GetUserSettings.cs b/src/Server/Coderr.Server.Api/Core/Users/Queries/GetUserSettings.cs similarity index 87% rename from src/Server/OneTrueError.Api/Core/Users/Queries/GetUserSettings.cs rename to src/Server/Coderr.Server.Api/Core/Users/Queries/GetUserSettings.cs index e365bedb..4f793040 100644 --- a/src/Server/OneTrueError.Api/Core/Users/Queries/GetUserSettings.cs +++ b/src/Server/Coderr.Server.Api/Core/Users/Queries/GetUserSettings.cs @@ -1,22 +1,23 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Users.Queries -{ - /// - /// Get settings for an user. - /// - public class GetUserSettings : Query - { - /// - /// Get user settings for this application only - /// - public int ApplicationId { get; set; } - - - /// - /// User to get settings for - /// - [IgnoreField] - public int UserId { get; set; } - } +using DotNetCqs; + +namespace Coderr.Server.Api.Core.Users.Queries +{ + /// + /// Get settings for an user. + /// + [Message] + public class GetUserSettings : Query + { + /// + /// Get user settings for this application only + /// + public int ApplicationId { get; set; } + + + /// + /// User to get settings for + /// + [IgnoreField] + public int UserId { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Core/Users/Queries/GetUserSettingsResult.cs b/src/Server/Coderr.Server.Api/Core/Users/Queries/GetUserSettingsResult.cs new file mode 100644 index 00000000..d7d40266 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Core/Users/Queries/GetUserSettingsResult.cs @@ -0,0 +1,38 @@ +namespace Coderr.Server.Api.Core.Users.Queries +{ + /// + /// Result for + /// + /// + /// + /// All settings are system wide except for . + /// + /// + public class GetUserSettingsResult + { + /// + /// From the user account, always specified. + /// + public string EmailAddress { get; set; } + + /// + /// First name (optional) + /// + public string FirstName { get; set; } + + /// + /// Last name (optional) + /// + public string LastName { get; set; } + + /// + /// Cell phone number (optional, but required for text notifications). + /// + public string MobileNumber { get; set; } + + /// + /// Application specific settings + /// + public NotificationSettings Notifications { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Users/ReadMe.md b/src/Server/Coderr.Server.Api/Core/Users/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/Core/Users/ReadMe.md rename to src/Server/Coderr.Server.Api/Core/Users/ReadMe.md diff --git a/src/Server/OneTrueError.Api/Core/EnumExtensions.cs b/src/Server/Coderr.Server.Api/EnumExtensions.cs similarity index 95% rename from src/Server/OneTrueError.Api/Core/EnumExtensions.cs rename to src/Server/Coderr.Server.Api/EnumExtensions.cs index 30b40ad7..1b39b308 100644 --- a/src/Server/OneTrueError.Api/Core/EnumExtensions.cs +++ b/src/Server/Coderr.Server.Api/EnumExtensions.cs @@ -1,37 +1,37 @@ -using System; - -namespace OneTrueError.Api.Core -{ - /// - /// Extensions making it easier to work with enums - /// - public static class EnumExtensions - { - /// - /// Convert from one enum type to another - /// - /// Type to convert to - /// source - /// Converted enum value - /// - /// - /// Does the conversion by translating the value to a string and then parsing it. That chocie was made - /// since the same value might exist in both enums by representing different fields. - /// - /// - /// Source enum value was not found in the target type. - public static TTo ConvertEnum(this Enum source) where TTo : struct - { - var str = source.ToString(); - TTo result; - if (!Enum.TryParse(str, true, out result)) - { - throw new FormatException( - string.Format("Cannot convert enum of type '{0}' with value '{1}' to enum type '{2}'.", - source.GetType().FullName, source, typeof(TTo).FullName)); - } - - return result; - } - } +using System; + +namespace Coderr.Server.Api +{ + /// + /// Extensions making it easier to work with enums + /// + public static class EnumExtensions + { + /// + /// Convert from one enum type to another + /// + /// Type to convert to + /// source + /// Converted enum value + /// + /// + /// Does the conversion by translating the value to a string and then parsing it. That chocie was made + /// since the same value might exist in both enums by representing different fields. + /// + /// + /// Source enum value was not found in the target type. + public static TTo ConvertEnum(this Enum source) where TTo : struct + { + var str = source.ToString(); + TTo result; + if (!Enum.TryParse(str, true, out result)) + { + throw new FormatException( + string.Format("Cannot convert enum of type '{0}' with value '{1}' to enum type '{2}'.", + source.GetType().FullName, source, typeof(TTo).FullName)); + } + + return result; + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/EventAttribute.cs b/src/Server/Coderr.Server.Api/EventAttribute.cs new file mode 100644 index 00000000..bb72ae6a --- /dev/null +++ b/src/Server/Coderr.Server.Api/EventAttribute.cs @@ -0,0 +1,10 @@ +namespace Coderr.Server.Api +{ + /// + /// Marks a DTO as an event. + /// + public class EventAttribute : MessageAttribute + { + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/IgnoreFieldAttribute.cs b/src/Server/Coderr.Server.Api/IgnoreFieldAttribute.cs new file mode 100644 index 00000000..f517be69 --- /dev/null +++ b/src/Server/Coderr.Server.Api/IgnoreFieldAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Coderr.Server.Api +{ + /// + /// Used to make the typescript compiler ignore certain properties and types. + /// + public class IgnoreFieldAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/MessageAttribute.cs b/src/Server/Coderr.Server.Api/MessageAttribute.cs new file mode 100644 index 00000000..2affc1d6 --- /dev/null +++ b/src/Server/Coderr.Server.Api/MessageAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Coderr.Server.Api +{ + /// + /// Used to mark classes as DTOs (to be able to index and process them) + /// + public class MessageAttribute : Attribute + { + } +} diff --git a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilarities.cs b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilarities.cs similarity index 88% rename from src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilarities.cs rename to src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilarities.cs index 03caed99..10797cd4 100644 --- a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilarities.cs +++ b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilarities.cs @@ -1,35 +1,36 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.ContextData.Queries -{ - /// - /// Get similarities (i.e. analyzed context collections where we have normalized values and checked which values are - /// more frequently occuring). - /// - public class GetSimilarities : Query - { - /// - /// Serialization constructor - /// - protected GetSimilarities() - { - } - - /// - /// Creates a new instance of . - /// - /// incident to get similarities for - /// incidentId - public GetSimilarities(int incidentId) - { - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - } - - /// - /// incident to get similarities for - /// - public int IncidentId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.ContextData.Queries +{ + /// + /// Get similarities (i.e. analyzed context collections where we have normalized values and checked which values are + /// more frequently occurring). + /// + [Message] + public class GetSimilarities : Query + { + /// + /// Serialization constructor + /// + protected GetSimilarities() + { + } + + /// + /// Creates a new instance of . + /// + /// incident to get similarities for + /// incidentId + public GetSimilarities(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + } + + /// + /// incident to get similarities for + /// + public int IncidentId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesCollection.cs b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesCollection.cs similarity index 92% rename from src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesCollection.cs rename to src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesCollection.cs index 357dd92a..56010ba6 100644 --- a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesCollection.cs +++ b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesCollection.cs @@ -1,38 +1,38 @@ -using System; -using System.Collections.Generic; - -namespace OneTrueError.Api.Modules.ContextData.Queries -{ - /// - /// Context collection for . - /// - public class GetSimilaritiesCollection - { - private List _similarities = new List(); - - /// - /// Name of this collection. - /// - public string Name { get; set; } - - /// - /// An analyzed property and all its values. - /// - public GetSimilaritiesSimilarity[] Similarities - { - get { return _similarities.ToArray(); } - set { _similarities = new List(value); } - } - - /// - /// Add an analyzed property. - /// - /// property + values - /// similarity - public void Add(GetSimilaritiesSimilarity similarity) - { - if (similarity == null) throw new ArgumentNullException("similarity"); - _similarities.Add(similarity); - } - } +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Modules.ContextData.Queries +{ + /// + /// Context collection for . + /// + public class GetSimilaritiesCollection + { + private List _similarities = new List(); + + /// + /// Name of this collection. + /// + public string Name { get; set; } + + /// + /// An analyzed property and all its values. + /// + public GetSimilaritiesSimilarity[] Similarities + { + get { return _similarities.ToArray(); } + set { _similarities = new List(value); } + } + + /// + /// Add an analyzed property. + /// + /// property + values + /// similarity + public void Add(GetSimilaritiesSimilarity similarity) + { + if (similarity == null) throw new ArgumentNullException("similarity"); + _similarities.Add(similarity); + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesResult.cs b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesResult.cs similarity index 81% rename from src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesResult.cs rename to src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesResult.cs index 2785186e..aa2fd331 100644 --- a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesResult.cs +++ b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesResult.cs @@ -1,13 +1,13 @@ -namespace OneTrueError.Api.Modules.ContextData.Queries -{ - /// - /// Result for . - /// - public class GetSimilaritiesResult - { - /// - /// All analyzed context collections - /// - public GetSimilaritiesCollection[] Collections { get; set; } - } +namespace Coderr.Server.Api.Modules.ContextData.Queries +{ + /// + /// Result for . + /// + public class GetSimilaritiesResult + { + /// + /// All analyzed context collections + /// + public GetSimilaritiesCollection[] Collections { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesSimilarity.cs b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesSimilarity.cs similarity index 92% rename from src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesSimilarity.cs rename to src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesSimilarity.cs index 5e442294..81e487af 100644 --- a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesSimilarity.cs +++ b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesSimilarity.cs @@ -1,41 +1,41 @@ -using System; - -namespace OneTrueError.Api.Modules.ContextData.Queries -{ - /// - /// A property in . - /// - public class GetSimilaritiesSimilarity - { - /// - /// Creates a new instance of . - /// - /// Property name - /// name - public GetSimilaritiesSimilarity(string name) - { - if (name == null) throw new ArgumentNullException("name"); - Name = name; - Values = new GetSimilaritiesValue[0]; - } - - /// - /// Serialization constructor. - /// - protected GetSimilaritiesSimilarity() - { - Values = new GetSimilaritiesValue[0]; - } - - /// - /// Name of this similarity. - /// - public string Name { get; private set; } - - - /// - /// The different values that this one have got. - /// - public GetSimilaritiesValue[] Values { get; set; } - } +using System; + +namespace Coderr.Server.Api.Modules.ContextData.Queries +{ + /// + /// A property in . + /// + public class GetSimilaritiesSimilarity + { + /// + /// Creates a new instance of . + /// + /// Property name + /// name + public GetSimilaritiesSimilarity(string name) + { + if (name == null) throw new ArgumentNullException("name"); + Name = name; + Values = new GetSimilaritiesValue[0]; + } + + /// + /// Serialization constructor. + /// + protected GetSimilaritiesSimilarity() + { + Values = new GetSimilaritiesValue[0]; + } + + /// + /// Name of this similarity. + /// + public string Name { get; private set; } + + + /// + /// The different values that this one have got. + /// + public GetSimilaritiesValue[] Values { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesValue.cs b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesValue.cs similarity index 93% rename from src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesValue.cs rename to src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesValue.cs index a437c89e..462a3502 100644 --- a/src/Server/OneTrueError.Api/Modules/ContextData/Queries/GetSimilaritiesValue.cs +++ b/src/Server/Coderr.Server.Api/Modules/ContextData/Queries/GetSimilaritiesValue.cs @@ -1,44 +1,44 @@ -using System; - -namespace OneTrueError.Api.Modules.ContextData.Queries -{ - /// - /// A single value for . - /// - public class GetSimilaritiesValue - { - /// - /// Creates a new instance of . - /// - /// Value, null is allowed - /// 0-100 - /// Number of times that this value have been received. - /// - public GetSimilaritiesValue(string value, int percentage, int count) - { - if (percentage < 0 || percentage > 100) - throw new ArgumentOutOfRangeException("percentage", percentage, - "Percentage should be between 0 and 100."); - if (count <= 0) throw new ArgumentOutOfRangeException("count"); - - Value = value; - Percentage = percentage; - Count = count; - } - - /// - /// Number of times that this value have been found in an error report. - /// - public int Count { get; set; } - - /// - /// 0-100 - /// - public int Percentage { get; set; } - - /// - /// Value for this item - /// - public string Value { get; set; } - } +using System; + +namespace Coderr.Server.Api.Modules.ContextData.Queries +{ + /// + /// A single value for . + /// + public class GetSimilaritiesValue + { + /// + /// Creates a new instance of . + /// + /// Value, null is allowed + /// 0-100 + /// Number of times that this value have been received. + /// + public GetSimilaritiesValue(string value, int percentage, int count) + { + if (percentage < 0 || percentage > 100) + throw new ArgumentOutOfRangeException("percentage", percentage, + "Percentage should be between 0 and 100."); + if (count <= 0) throw new ArgumentOutOfRangeException("count"); + + Value = value; + Percentage = percentage; + Count = count; + } + + /// + /// Number of times that this value have been found in an error report. + /// + public int Count { get; set; } + + /// + /// 0-100 + /// + public int Percentage { get; set; } + + /// + /// Value for this item + /// + public string Value { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncident.cs b/src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncident.cs similarity index 91% rename from src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncident.cs rename to src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncident.cs index e321f584..6af85cd6 100644 --- a/src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncident.cs +++ b/src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncident.cs @@ -1,34 +1,35 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.ErrorOrigins.Queries -{ - /// - /// Get all error origins for the specified incident. - /// - public class GetOriginsForIncident : Query - { - /// - /// Creates a new instance of . - /// - /// incident to get error origins for - /// incidentId < 1 - public GetOriginsForIncident(int incidentId) - { - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - } - - /// - /// Serialization constructor - /// - protected GetOriginsForIncident() - { - } - - /// - /// Incident to get origins for - /// - public int IncidentId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.ErrorOrigins.Queries +{ + /// + /// Get all error origins for the specified incident. + /// + [Message] + public class GetOriginsForIncident : Query + { + /// + /// Creates a new instance of . + /// + /// incident to get error origins for + /// incidentId < 1 + public GetOriginsForIncident(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + } + + /// + /// Serialization constructor + /// + protected GetOriginsForIncident() + { + } + + /// + /// Incident to get origins for + /// + public int IncidentId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResult.cs b/src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResult.cs similarity index 81% rename from src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResult.cs rename to src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResult.cs index 427d7e8e..18ab387b 100644 --- a/src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResult.cs +++ b/src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResult.cs @@ -1,13 +1,13 @@ -namespace OneTrueError.Api.Modules.ErrorOrigins.Queries -{ - /// - /// Result for . - /// - public class GetOriginsForIncidentResult - { - /// - /// One item per geographic location - /// - public GetOriginsForIncidentResultItem[] Items { get; set; } - } +namespace Coderr.Server.Api.Modules.ErrorOrigins.Queries +{ + /// + /// Result for . + /// + public class GetOriginsForIncidentResult + { + /// + /// One item per geographic location + /// + public GetOriginsForIncidentResultItem[] Items { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResultItem.cs b/src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResultItem.cs similarity index 87% rename from src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResultItem.cs rename to src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResultItem.cs index b6bc276b..f8e4bb21 100644 --- a/src/Server/OneTrueError.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResultItem.cs +++ b/src/Server/Coderr.Server.Api/Modules/ErrorOrigins/Queries/GetOriginsForIncidentResultItem.cs @@ -1,23 +1,23 @@ -namespace OneTrueError.Api.Modules.ErrorOrigins.Queries -{ - /// - /// Item for . - /// - public class GetOriginsForIncidentResultItem - { - /// - /// Latitude - /// - public double Latitude { get; set; } - - /// - /// Longitude - /// - public double Longitude { get; set; } - - /// - /// Number of error reports that have been received from this incident - /// - public int NumberOfErrorReports { get; set; } - } +namespace Coderr.Server.Api.Modules.ErrorOrigins.Queries +{ + /// + /// Item for . + /// + public class GetOriginsForIncidentResultItem + { + /// + /// Latitude + /// + public double Latitude { get; set; } + + /// + /// Longitude + /// + public double Longitude { get; set; } + + /// + /// Number of error reports that have been received from this incident + /// + public int NumberOfErrorReports { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentStateSummary.cs b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentStateSummary.cs new file mode 100644 index 00000000..32263751 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentStateSummary.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.Api.Modules.History.Queries +{ + [Message] + public class GetIncidentStateSummary + { + public int ApplicationId { get; set; } + public string ApplicationVersion { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentStateSummaryResult.cs b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentStateSummaryResult.cs new file mode 100644 index 00000000..da81a0f7 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentStateSummaryResult.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.Api.Modules.History.Queries +{ + public class GetIncidentStateSummaryResult + { + public int ClosedCount { get; set; } + public int NewCount { get; set; } + public int ReOpenedCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForState.cs b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForState.cs new file mode 100644 index 00000000..6e317a08 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForState.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Text; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.History.Queries +{ + [Message] + public class GetIncidentsForStates : Query + { + public string ApplicationVersion { get; set; } + public int ApplicationId { get; set; } + + public bool IsClosed { get; set; } + public bool IsNew { get; set; } + + public bool IsReopened { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForStatesResult.cs b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForStatesResult.cs new file mode 100644 index 00000000..04b0b705 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForStatesResult.cs @@ -0,0 +1,7 @@ +namespace Coderr.Server.Api.Modules.History.Queries +{ + public class GetIncidentsForStatesResult + { + public GetIncidentsForStatesResultItem[] Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForStatesResultItem.cs b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForStatesResultItem.cs new file mode 100644 index 00000000..ded7afcc --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/History/Queries/GetIncidentsForStatesResultItem.cs @@ -0,0 +1,16 @@ +using System; + +namespace Coderr.Server.Api.Modules.History.Queries +{ + public class GetIncidentsForStatesResultItem + { + public int IncidentId { get; set; } + public string IncidentName { get; set; } + public DateTime CreatedAtUtc { get; set; } + + public bool IsClosed { get; set; } + public bool IsNew { get; set; } + + public bool IsReopened { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogs.cs b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogs.cs new file mode 100644 index 00000000..0687ef1f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogs.cs @@ -0,0 +1,38 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Logs.Queries +{ + [Message] + public class GetLogs : Query + { + public GetLogs(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + IncidentId = incidentId; + } + + public GetLogs(int incidentId, int reportId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + if (reportId <= 0) throw new ArgumentOutOfRangeException(nameof(reportId)); + IncidentId = incidentId; + ReportId = reportId; + } + + protected GetLogs() + { + + } + + /// + /// Incident to check. + /// + public int IncidentId { get; private set; } + + /// + /// Check for a specific report (if set). + /// + public int? ReportId { get; private set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResult.cs b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResult.cs new file mode 100644 index 00000000..fa220056 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResult.cs @@ -0,0 +1,7 @@ +namespace Coderr.Server.Api.Modules.Logs.Queries +{ + public class GetLogsResult + { + public GetLogsResultEntry[] Entries { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResultEntry.cs b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResultEntry.cs new file mode 100644 index 00000000..a8a6bd9a --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResultEntry.cs @@ -0,0 +1,15 @@ +using System; + +namespace Coderr.Server.Api.Modules.Logs.Queries +{ + public class GetLogsResultEntry + { + public DateTime TimeStampUtc { get; set; } + + public string Message { get; set; } + + public GetLogsResultEntryLevel Level { get; set; } + + public string Exception { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResultEntryLevel.cs b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResultEntryLevel.cs new file mode 100644 index 00000000..99c6be59 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/GetLogsResultEntryLevel.cs @@ -0,0 +1,12 @@ +namespace Coderr.Server.Api.Modules.Logs.Queries +{ + public enum GetLogsResultEntryLevel + { + Trace = 1, + Debug = 2, + Info = 3, + Warning = 4, + Error = 5, + Critical = 6 + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Logs/Queries/HasLogs.cs b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/HasLogs.cs new file mode 100644 index 00000000..2615de86 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/HasLogs.cs @@ -0,0 +1,41 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Logs.Queries +{ + /// + /// Check if an incident (or a specific report for that incident) has logs attached to it. + /// + [Message] + public class HasLogs : Query + { + public HasLogs(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + IncidentId = incidentId; + } + + public HasLogs(int incidentId, int reportId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId)); + if (reportId <= 0) throw new ArgumentOutOfRangeException(nameof(reportId)); + IncidentId = incidentId; + ReportId = reportId; + } + + protected HasLogs() + { + + } + + /// + /// Incident to check. + /// + public int IncidentId { get; private set; } + + /// + /// Check for a specific report (if set). + /// + public int? ReportId { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Logs/Queries/HasLogsReply.cs b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/HasLogsReply.cs new file mode 100644 index 00000000..5401dc4f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Logs/Queries/HasLogsReply.cs @@ -0,0 +1,7 @@ +namespace Coderr.Server.Api.Modules.Logs.Queries +{ + public class HasLogsReply + { + public bool HasLogs { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidents.cs b/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidents.cs new file mode 100644 index 00000000..bfc8df3f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidents.cs @@ -0,0 +1,16 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Mine.Queries +{ + /// + /// Get the users assigned incidents and the ones that are recommended for that person. + /// + [Message] + public class ListMyIncidents : Query + { + /// + /// Limit to the given application (if specified). + /// + public int? ApplicationId { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidentsResult.cs b/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidentsResult.cs new file mode 100644 index 00000000..54dc1271 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidentsResult.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Api.Modules.Mine.Queries +{ + /// + /// Result for . + /// + public class ListMyIncidentsResult + { + public string Comment { get; set; } + public IList Items { get; set; } + public IList Suggestions { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidentsResultItem.cs b/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidentsResultItem.cs new file mode 100644 index 00000000..d7d09106 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMyIncidentsResultItem.cs @@ -0,0 +1,72 @@ +using System; + +namespace Coderr.Server.Api.Modules.Mine.Queries +{ + /// + /// Item for . + /// + public class ListMyIncidentsResultItem + { + /// + /// Creates new instance of . + /// + /// incident id + /// incident name + public ListMyIncidentsResultItem(int id, string name) + { + if (name == null) throw new ArgumentNullException("name"); + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + Id = id; + Name = name; + } + + /// + /// Serialization constructor + /// + protected ListMyIncidentsResultItem() + { + } + + /// + /// Id of the application that this incident belongs to + /// + public int ApplicationId { get; set; } + + /// + /// Name of the application that this incident belongs to + /// + public string ApplicationName { get; set; } + + /// + /// when this incident was assigned to me. + /// + public DateTime AssignedAtUtc { get; set; } + + /// + /// When the first report was received. + /// + public DateTime CreatedAtUtc { get; set; } + + public int DaysOld => (int)DateTime.UtcNow.Subtract(CreatedAtUtc).TotalDays; + + /// + /// Incident id + /// + public int Id { get; private set; } + + /// + /// When the last report was received (or when the last user action was made) + /// + public DateTime LastReportAtUtc { get; set; } + + /// + /// Incident name + /// + public string Name { get; private set; } + + /// + /// Total number of received reports (increased even if the number of stored reports are at the limit) + /// + public int ReportCount { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMySuggestedItem.cs b/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMySuggestedItem.cs new file mode 100644 index 00000000..52bde240 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Mine/Queries/ListMySuggestedItem.cs @@ -0,0 +1,79 @@ +using System; + +namespace Coderr.Server.Api.Modules.Mine.Queries +{ + /// + /// Item for . + /// + public class ListMySuggestedItem + { + /// + /// Creates new instance of . + /// + /// incident id + /// incident name + public ListMySuggestedItem(int id, string name) + { + if (name == null) throw new ArgumentNullException("name"); + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + Id = id; + Name = name; + } + + /// + /// Serialization constructor + /// + protected ListMySuggestedItem() + { + } + + /// + /// Id of the application that this incident belongs to + /// + public int ApplicationId { get; set; } + + /// + /// Name of the application that this incident belongs to + /// + public string ApplicationName { get; set; } + + /// + /// When the first report was received. + /// + public DateTime CreatedAtUtc { get; set; } + + public string ExceptionTypeName { get; set; } + + /// + /// Incident id + /// + public int Id { get; private set; } + + /// + /// When the last report was received (or when the last user action was made) + /// + public DateTime LastReportAtUtc { get; set; } + + /// + /// Incident name + /// + public string Name { get; private set; } + + /// + /// Number of points for this item. the more the merrier. + /// + public int Weight { get; set; } + + /// + /// Total number of received reports (increased even if the number of stored reports are at the limit) + /// + public int ReportCount { get; set; } + + public string StackTrace { get; set; } + + /// + /// Why this item was suggested. + /// + public string Motivation { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Onboarding/Commands/SetOnboardingChoices.cs b/src/Server/Coderr.Server.Api/Modules/Onboarding/Commands/SetOnboardingChoices.cs new file mode 100644 index 00000000..1ea3c787 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Onboarding/Commands/SetOnboardingChoices.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Api.Modules.Onboarding.Commands +{ + /// + /// Set current state of the onboarding. + /// + [Command] + public class SetOnboardingChoices + { + /// + /// All libraries that the user wants to generate example incidents for. + /// + public IReadOnlyList Libraries { get; set; } + + /// + /// DOTNET or NODEJS + /// + public string MainLanguage { get; set; } + + public string Feedback { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Onboarding/Queries/GetOnboardingState.cs b/src/Server/Coderr.Server.Api/Modules/Onboarding/Queries/GetOnboardingState.cs new file mode 100644 index 00000000..6845116d --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Onboarding/Queries/GetOnboardingState.cs @@ -0,0 +1,17 @@ +using System; +using System.Text; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Onboarding.Queries +{ + + /// + /// Get current state of the onboarding process. + /// + /// The onboarding process is only run for the admin, and therefore these settings are site wide. + [Message] + public class GetOnboardingState : Query + { + + } +} diff --git a/src/Server/Coderr.Server.Api/Modules/Onboarding/Queries/GetOnboardingStateResult.cs b/src/Server/Coderr.Server.Api/Modules/Onboarding/Queries/GetOnboardingStateResult.cs new file mode 100644 index 00000000..c4a9257a --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Onboarding/Queries/GetOnboardingStateResult.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Api.Modules.Onboarding.Queries +{ + /// + /// Result for + /// + public class GetOnboardingStateResult + { + /// + /// Onboarding is completed. + /// + public bool IsComplete { get; set; } + + /// + /// Libraries that the user generated demo incidents for. + /// + public IReadOnlyList Libraries { get; set; } + + /// + /// DOTNET or NODEJS + /// + public string MainLanguage { get; set; } + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Tagging/Events/TagAttachedToIncident.cs b/src/Server/Coderr.Server.Api/Modules/Tagging/Events/TagAttachedToIncident.cs new file mode 100644 index 00000000..978d93cf --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Tagging/Events/TagAttachedToIncident.cs @@ -0,0 +1,45 @@ +using System; + +namespace Coderr.Server.Api.Modules.Tagging.Events +{ + /// + /// New tag(s) have been identified for the processed incident. + /// + [Message] + public class TagAttachedToIncident + { + /// + /// Creates a new instance of . + /// + /// Incident being processed + /// tags + /// tags + /// incidentId + public TagAttachedToIncident(int applicationId, int incidentId, string[] tags) + { + if (tags == null) throw new ArgumentNullException("tags"); + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); + ApplicationId = applicationId; + Tags = tags; + IncidentId = incidentId; + } + + protected TagAttachedToIncident() + { + + } + + /// + /// Incident being processed + /// + public int IncidentId { get; private set; } + + public int ApplicationId { get; private set; } + + /// + /// Identified tags + /// + public string[] Tags { get; private set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTags.cs b/src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTags.cs new file mode 100644 index 00000000..523002fe --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTags.cs @@ -0,0 +1,21 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Tagging.Queries +{ + /// + /// Get all tags that the system have identified for an incident. + /// + [Message] + public class GetTags : Query + { + /// + /// Application to get tags for + /// + public int? ApplicationId { get; set; } + + /// + /// Incident to get tags for + /// + public int? IncidentId { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTagsForApplication.cs b/src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTagsForApplication.cs new file mode 100644 index 00000000..8189b414 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTagsForApplication.cs @@ -0,0 +1,28 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Tagging.Queries +{ + /// + /// Get all tags that the system have identified for an incident. + /// + [Message] + public class GetTagsForApplication : Query + { + /// + /// Creates a new instance of . + /// + /// Incident to get tags for + /// incidentId + public GetTagsForApplication(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + ApplicationId = applicationId; + } + + /// + /// Incident to get tags for + /// + public int ApplicationId { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Tagging/Queries/GetTagsForIncident.cs b/src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTagsForIncident.cs similarity index 91% rename from src/Server/OneTrueError.Api/Modules/Tagging/Queries/GetTagsForIncident.cs rename to src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTagsForIncident.cs index bb1e359f..7171dc42 100644 --- a/src/Server/OneTrueError.Api/Modules/Tagging/Queries/GetTagsForIncident.cs +++ b/src/Server/Coderr.Server.Api/Modules/Tagging/Queries/GetTagsForIncident.cs @@ -1,27 +1,28 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.Tagging.Queries -{ - /// - /// Get all tags that the system have identified for an incident. - /// - public class GetTagsForIncident : Query - { - /// - /// Creates a new instance of . - /// - /// Incident to get tags for - /// incidentId - public GetTagsForIncident(int incidentId) - { - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - } - - /// - /// Incident to get tags for - /// - public int IncidentId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Tagging.Queries +{ + /// + /// Get all tags that the system have identified for an incident. + /// + [Message] + public class GetTagsForIncident : Query + { + /// + /// Creates a new instance of . + /// + /// Incident to get tags for + /// incidentId + public GetTagsForIncident(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + } + + /// + /// Incident to get tags for + /// + public int IncidentId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Tagging/TagDTO.cs b/src/Server/Coderr.Server.Api/Modules/Tagging/TagDTO.cs similarity index 85% rename from src/Server/OneTrueError.Api/Modules/Tagging/TagDTO.cs rename to src/Server/Coderr.Server.Api/Modules/Tagging/TagDTO.cs index 3605f46a..7b34354d 100644 --- a/src/Server/OneTrueError.Api/Modules/Tagging/TagDTO.cs +++ b/src/Server/Coderr.Server.Api/Modules/Tagging/TagDTO.cs @@ -1,18 +1,18 @@ -namespace OneTrueError.Api.Modules.Tagging -{ - /// - /// A stack overflow tag - /// - public class TagDTO - { - /// - /// Name - /// - public string Name { get; set; } - - /// - /// Used to sort tags before displaying them. - /// - public int OrderNumber { get; set; } - } +namespace Coderr.Server.Api.Modules.Tagging +{ + /// + /// A stack overflow tag + /// + public class TagDTO + { + /// + /// Name + /// + public string Name { get; set; } + + /// + /// Used to sort tags before displaying them. + /// + public int OrderNumber { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/Commands/CreateTrigger.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/Commands/CreateTrigger.cs similarity index 92% rename from src/Server/OneTrueError.Api/Modules/Triggers/Commands/CreateTrigger.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/Commands/CreateTrigger.cs index a5a3db74..55d35bdb 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/Commands/CreateTrigger.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/Commands/CreateTrigger.cs @@ -1,83 +1,83 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.Triggers.Commands -{ - /// - /// Create a new trigger - /// - public class CreateTrigger : Command - { - /// - /// Creates a new instance of . - /// - /// Application that the trigger is for. - /// Trigger name - public CreateTrigger(int applicationId, string name) - { - if (name == null) throw new ArgumentNullException("name"); - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - - ApplicationId = applicationId; - Name = name; - } - - /// - /// Serialization constructor - /// - protected CreateTrigger() - { - } - - /// - /// Actions to run - /// - public TriggerActionDataDTO[] Actions { get; set; } - - /// - /// Application that the trigger belongs to. - /// - public int ApplicationId { get; private set; } - - /// - /// What the trigger does and why - /// - public string Description { get; set; } - - /// - /// Primary key - /// - public int Id { get; set; } - - /// - /// Action to take after all have run. - /// - public LastTriggerActionDTO LastTriggerAction { get; set; } - - - /// - /// Trigger name. - /// - public string Name { get; private set; } - - /// - /// Rules that determine if this trigger can run. - /// - public TriggerRuleBase[] Rules { get; set; } - - /// - /// Run trigger for existing incidents (received a duplicate exception) - /// - public bool RunForExistingIncidents { get; set; } - - /// - /// Run trigger for new incidents (i.e. received a new unique exception) - /// - public bool RunForNewIncidents { get; set; } - - /// - /// Run for incidents that is closed but received a new error report. - /// - public bool RunForReOpenedIncidents { get; set; } - } +using System; + +namespace Coderr.Server.Api.Modules.Triggers.Commands +{ + /// + /// Create a new trigger + /// + [Message] + public class CreateTrigger + { + /// + /// Creates a new instance of . + /// + /// Application that the trigger is for. + /// Trigger name + public CreateTrigger(int applicationId, string name) + { + if (name == null) throw new ArgumentNullException("name"); + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + + ApplicationId = applicationId; + Name = name; + } + + /// + /// Serialization constructor + /// + protected CreateTrigger() + { + } + + /// + /// Actions to run + /// + public TriggerActionDataDTO[] Actions { get; set; } + + /// + /// Application that the trigger belongs to. + /// + public int ApplicationId { get; private set; } + + /// + /// What the trigger does and why + /// + public string Description { get; set; } + + /// + /// Primary key + /// + public int Id { get; set; } + + /// + /// Action to take after all have run. + /// + public LastTriggerActionDTO LastTriggerAction { get; set; } + + + /// + /// Trigger name. + /// + public string Name { get; private set; } + + /// + /// Rules that determine if this trigger can run. + /// + public TriggerRuleBase[] Rules { get; set; } + + /// + /// Run trigger for existing incidents (received a duplicate exception) + /// + public bool RunForExistingIncidents { get; set; } + + /// + /// Run trigger for new incidents (i.e. received a new unique exception) + /// + public bool RunForNewIncidents { get; set; } + + /// + /// Run for incidents that is closed but received a new error report. + /// + public bool RunForReOpenedIncidents { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/Commands/DeleteTrigger.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/Commands/DeleteTrigger.cs similarity index 79% rename from src/Server/OneTrueError.Api/Modules/Triggers/Commands/DeleteTrigger.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/Commands/DeleteTrigger.cs index 5eeb4cff..d17424f5 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/Commands/DeleteTrigger.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/Commands/DeleteTrigger.cs @@ -1,26 +1,26 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.Triggers.Commands -{ - /// - /// Delete a trigger - /// - public class DeleteTrigger : Command - { - /// - /// Creates a new instance of . - /// - /// primary key - public DeleteTrigger(int id) - { - if (id <= 0) throw new ArgumentOutOfRangeException("id"); - Id = id; - } - - /// - /// Primary key - /// - public int Id { get; private set; } - } +using System; + +namespace Coderr.Server.Api.Modules.Triggers.Commands +{ + /// + /// Delete a trigger + /// + [Message] + public class DeleteTrigger + { + /// + /// Creates a new instance of . + /// + /// primary key + public DeleteTrigger(int id) + { + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + Id = id; + } + + /// + /// Primary key + /// + public int Id { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/Commands/UpdateTrigger.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/Commands/UpdateTrigger.cs similarity index 91% rename from src/Server/OneTrueError.Api/Modules/Triggers/Commands/UpdateTrigger.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/Commands/UpdateTrigger.cs index 298e5434..e3e88deb 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/Commands/UpdateTrigger.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/Commands/UpdateTrigger.cs @@ -1,78 +1,78 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.Triggers.Commands -{ - /// - /// Update an existing trigger - /// - public class UpdateTrigger : Command - { - /// - /// Creates a new instance of . - /// - /// trigger identity. - /// Trigger name - public UpdateTrigger(int id, string name) - { - if (name == null) throw new ArgumentNullException("name"); - if (id <= 0) throw new ArgumentOutOfRangeException("id"); - - Id = id; - Name = name; - } - - /// - /// Serialization constructor - /// - protected UpdateTrigger() - { - } - - /// - /// Actions to run - /// - public TriggerActionDataDTO[] Actions { get; set; } - - /// - /// What the trigger does and why - /// - public string Description { get; set; } - - /// - /// Primary key - /// - public int Id { get; private set; } - - /// - /// Action to take after all have run. - /// - public LastTriggerActionDTO LastTriggerAction { get; set; } - - - /// - /// Trigger name. - /// - public string Name { get; private set; } - - /// - /// Rules that determine if this trigger can run. - /// - public TriggerRuleBase[] Rules { get; set; } - - /// - /// Run trigger for existing incidents (received a duplicate exception) - /// - public bool RunForExistingIncidents { get; set; } - - /// - /// Run trigger for new incidents (i.e. received a new unique exception) - /// - public bool RunForNewIncidents { get; set; } - - /// - /// Run for incidents that is closed but received a new error report. - /// - public bool RunForReOpenedIncidents { get; set; } - } +using System; + +namespace Coderr.Server.Api.Modules.Triggers.Commands +{ + /// + /// Update an existing trigger + /// + [Message] + public class UpdateTrigger + { + /// + /// Creates a new instance of . + /// + /// trigger identity. + /// Trigger name + public UpdateTrigger(int id, string name) + { + if (name == null) throw new ArgumentNullException("name"); + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + + Id = id; + Name = name; + } + + /// + /// Serialization constructor + /// + protected UpdateTrigger() + { + } + + /// + /// Actions to run + /// + public TriggerActionDataDTO[] Actions { get; set; } + + /// + /// What the trigger does and why + /// + public string Description { get; set; } + + /// + /// Primary key + /// + public int Id { get; private set; } + + /// + /// Action to take after all have run. + /// + public LastTriggerActionDTO LastTriggerAction { get; set; } + + + /// + /// Trigger name. + /// + public string Name { get; private set; } + + /// + /// Rules that determine if this trigger can run. + /// + public TriggerRuleBase[] Rules { get; set; } + + /// + /// Run trigger for existing incidents (received a duplicate exception) + /// + public bool RunForExistingIncidents { get; set; } + + /// + /// Run trigger for new incidents (i.e. received a new unique exception) + /// + public bool RunForNewIncidents { get; set; } + + /// + /// Run for incidents that is closed but received a new error report. + /// + public bool RunForReOpenedIncidents { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/LastTriggerActionDTO.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/LastTriggerActionDTO.cs similarity index 84% rename from src/Server/OneTrueError.Api/Modules/Triggers/LastTriggerActionDTO.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/LastTriggerActionDTO.cs index a490a6e2..dc84aec8 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/LastTriggerActionDTO.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/LastTriggerActionDTO.cs @@ -1,18 +1,18 @@ -namespace OneTrueError.Api.Modules.Triggers -{ - /// - /// What to do if all rules accepted the report. - /// - public enum LastTriggerActionDTO - { - /// - /// Execute trigger actions. - /// - ExecuteActions, - - /// - /// Abort the trigger - /// - AbortTrigger - } +namespace Coderr.Server.Api.Modules.Triggers +{ + /// + /// What to do if all rules accepted the report. + /// + public enum LastTriggerActionDTO + { + /// + /// Execute trigger actions. + /// + ExecuteActions, + + /// + /// Abort the trigger + /// + AbortTrigger + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetContextCollectionMetadata.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetContextCollectionMetadata.cs similarity index 91% rename from src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetContextCollectionMetadata.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetContextCollectionMetadata.cs index e6ccc33d..c021454b 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetContextCollectionMetadata.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetContextCollectionMetadata.cs @@ -1,27 +1,28 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.Triggers.Queries -{ - /// - /// Get metadata (context collection information) - /// - public class GetContextCollectionMetadata : Query - { - /// - /// Creates a new instance of . - /// - /// applicationId - /// applicationId - public GetContextCollectionMetadata(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - } - - /// - /// Application to get info for. - /// - public int ApplicationId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Triggers.Queries +{ + /// + /// Get metadata (context collection information) + /// + [Message] + public class GetContextCollectionMetadata : Query + { + /// + /// Creates a new instance of . + /// + /// applicationId + /// applicationId + public GetContextCollectionMetadata(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + ApplicationId = applicationId; + } + + /// + /// Application to get info for. + /// + public int ApplicationId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetContextCollectionMetadataItem.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetContextCollectionMetadataItem.cs similarity index 85% rename from src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetContextCollectionMetadataItem.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetContextCollectionMetadataItem.cs index e1d879d9..aee9a2b6 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetContextCollectionMetadataItem.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetContextCollectionMetadataItem.cs @@ -1,18 +1,18 @@ -namespace OneTrueError.Api.Modules.Triggers.Queries -{ - /// - /// Result item for - /// - public class GetContextCollectionMetadataItem - { - /// - /// Context name - /// - public string Name { get; set; } - - /// - /// Property names - /// - public string[] Properties { get; set; } - } +namespace Coderr.Server.Api.Modules.Triggers.Queries +{ + /// + /// Result item for + /// + public class GetContextCollectionMetadataItem + { + /// + /// Context name + /// + public string Name { get; set; } + + /// + /// Property names + /// + public string[] Properties { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTrigger.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTrigger.cs similarity index 90% rename from src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTrigger.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTrigger.cs index 49f45340..90562237 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTrigger.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTrigger.cs @@ -1,34 +1,35 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.Triggers.Queries -{ - /// - /// Get a configured trigger - /// - public class GetTrigger : Query - { - /// - /// Creates a new instance of . - /// - /// trigger id - /// id - public GetTrigger(int id) - { - if (id <= 0) throw new ArgumentOutOfRangeException("id"); - Id = id; - } - - /// - /// Serialization constructor - /// - protected GetTrigger() - { - } - - /// - /// Triggger id - /// - public int Id { get; set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Triggers.Queries +{ + /// + /// Get a configured trigger + /// + [Message] + public class GetTrigger : Query + { + /// + /// Creates a new instance of . + /// + /// trigger id + /// id + public GetTrigger(int id) + { + if (id <= 0) throw new ArgumentOutOfRangeException("id"); + Id = id; + } + + /// + /// Serialization constructor + /// + protected GetTrigger() + { + } + + /// + /// Triggger id + /// + public int Id { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTriggerDTO.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTriggerDTO.cs similarity index 93% rename from src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTriggerDTO.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTriggerDTO.cs index f44a4955..76cd7837 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTriggerDTO.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTriggerDTO.cs @@ -1,58 +1,58 @@ -namespace OneTrueError.Api.Modules.Triggers.Queries -{ - /// - /// Result for - /// - public class GetTriggerDTO - { - /// - /// Actions to take if all rules says OK. - /// - public TriggerActionDataDTO[] Actions { get; set; } - - /// - /// Application that the trigger is for. - /// - public int ApplicationId { get; set; } - - /// - /// What the trigger does. - /// - public string Description { get; set; } - - /// - /// Trigger id - /// - public int Id { get; set; } - - /// - /// Decision to use if all rules have been passed. - /// - public LastTriggerActionDTO LastTriggerAction { get; set; } - - /// - /// Trigger name - /// - public string Name { get; set; } - - /// - /// Rules deciding if actions can be run. - /// - public TriggerRuleBase[] Rules { get; set; } - - /// - /// Run for incidents that already has one or more reports. - /// - public bool RunForExistingIncidents { get; set; } - - /// - /// Run trigger for new incidents (got a new unqiue exception) - /// - public bool RunForNewIncidents { get; set; } - - /// - /// Run when a closed incident get its first new report. - /// - public bool RunForReOpenedIncidents { get; set; } - } +namespace Coderr.Server.Api.Modules.Triggers.Queries +{ + /// + /// Result for + /// + public class GetTriggerDTO + { + /// + /// Actions to take if all rules says OK. + /// + public TriggerActionDataDTO[] Actions { get; set; } + + /// + /// Application that the trigger is for. + /// + public int ApplicationId { get; set; } + + /// + /// What the trigger does. + /// + public string Description { get; set; } + + /// + /// Trigger id + /// + public int Id { get; set; } + + /// + /// Decision to use if all rules have been passed. + /// + public LastTriggerActionDTO LastTriggerAction { get; set; } + + /// + /// Trigger name + /// + public string Name { get; set; } + + /// + /// Rules deciding if actions can be run. + /// + public TriggerRuleBase[] Rules { get; set; } + + /// + /// Run for incidents that already has one or more reports. + /// + public bool RunForExistingIncidents { get; set; } + + /// + /// Run trigger for new incidents (got a new unqiue exception) + /// + public bool RunForNewIncidents { get; set; } + + /// + /// Run when a closed incident get its first new report. + /// + public bool RunForReOpenedIncidents { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTriggersForApplication.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTriggersForApplication.cs similarity index 92% rename from src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTriggersForApplication.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTriggersForApplication.cs index 1647b5a8..e861069f 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/Queries/GetTriggersForApplication.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/Queries/GetTriggersForApplication.cs @@ -1,34 +1,35 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.Triggers.Queries -{ - /// - /// Get all triggers for an application - /// - public class GetTriggersForApplication : Query - { - /// - /// Creates a new instance of . - /// - /// application to get triggers for - /// applicationId - public GetTriggersForApplication(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - } - - /// - /// Serialization constructor. - /// - protected GetTriggersForApplication() - { - } - - /// - /// Application - /// - public int ApplicationId { get; set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Triggers.Queries +{ + /// + /// Get all triggers for an application + /// + [Message] + public class GetTriggersForApplication : Query + { + /// + /// Creates a new instance of . + /// + /// application to get triggers for + /// applicationId + public GetTriggersForApplication(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + ApplicationId = applicationId; + } + + /// + /// Serialization constructor. + /// + protected GetTriggersForApplication() + { + } + + /// + /// Application + /// + public int ApplicationId { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerActionDataDTO.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerActionDataDTO.cs similarity index 84% rename from src/Server/OneTrueError.Api/Modules/Triggers/TriggerActionDataDTO.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/TriggerActionDataDTO.cs index 2b1f5d90..198c3f06 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerActionDataDTO.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerActionDataDTO.cs @@ -1,18 +1,18 @@ -namespace OneTrueError.Api.Modules.Triggers -{ - /// - /// DTO - /// - public class TriggerActionDataDTO - { - /// - /// Action context - /// - public string ActionContext { get; set; } - - /// - /// Action name - /// - public string ActionName { get; set; } - } +namespace Coderr.Server.Api.Modules.Triggers +{ + /// + /// DTO + /// + public class TriggerActionDataDTO + { + /// + /// Action context + /// + public string ActionContext { get; set; } + + /// + /// Action name + /// + public string ActionName { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerContextRule.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerContextRule.cs similarity index 88% rename from src/Server/OneTrueError.Api/Modules/Triggers/TriggerContextRule.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/TriggerContextRule.cs index a004d48e..1543db92 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerContextRule.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerContextRule.cs @@ -1,23 +1,23 @@ -namespace OneTrueError.Api.Modules.Triggers -{ - /// - /// Context when doing the filtering - /// - public class TriggerContextRule : TriggerRuleBase - { - /// - /// Context name currently being inspected - /// - public string ContextName { get; set; } - - /// - /// Property in that context - /// - public string PropertyName { get; set; } - - /// - /// Value - /// - public string PropertyValue { get; set; } - } +namespace Coderr.Server.Api.Modules.Triggers +{ + /// + /// Context when doing the filtering + /// + public class TriggerContextRule : TriggerRuleBase + { + /// + /// Context name currently being inspected + /// + public string ContextName { get; set; } + + /// + /// Property in that context + /// + public string PropertyName { get; set; } + + /// + /// Value + /// + public string PropertyValue { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerDTO.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerDTO.cs similarity index 89% rename from src/Server/OneTrueError.Api/Modules/Triggers/TriggerDTO.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/TriggerDTO.cs index 7502c2c0..88ebbd9b 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerDTO.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerDTO.cs @@ -1,28 +1,28 @@ -namespace OneTrueError.Api.Modules.Triggers -{ - /// - /// Trigger DTO - /// - public class TriggerDTO - { - /// - /// Description (typically why it was created and what it should do) - /// - public string Description { get; set; } - - /// - /// Identity - /// - public string Id { get; set; } - - /// - /// Trigger name - /// - public string Name { get; set; } - - /// - /// Short summary - /// - public string Summary { get; set; } - } +namespace Coderr.Server.Api.Modules.Triggers +{ + /// + /// Trigger DTO + /// + public class TriggerDTO + { + /// + /// Description (typically why it was created and what it should do) + /// + public string Description { get; set; } + + /// + /// Identity + /// + public string Id { get; set; } + + /// + /// Trigger name + /// + public string Name { get; set; } + + /// + /// Short summary + /// + public string Summary { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerExceptionRule.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerExceptionRule.cs similarity index 88% rename from src/Server/OneTrueError.Api/Modules/Triggers/TriggerExceptionRule.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/TriggerExceptionRule.cs index 730da072..24551c92 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerExceptionRule.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerExceptionRule.cs @@ -1,18 +1,18 @@ -namespace OneTrueError.Api.Modules.Triggers -{ - /// - /// Make a decision based on exception information - /// - public class TriggerExceptionRule : TriggerRuleBase - { - /// - /// Field in the exception details that should be inspected (property name from the Exception class) - /// - public string FieldName { get; set; } - - /// - /// Value that should be matched. - /// - public string Value { get; set; } - } +namespace Coderr.Server.Api.Modules.Triggers +{ + /// + /// Make a decision based on exception information + /// + public class TriggerExceptionRule : TriggerRuleBase + { + /// + /// Field in the exception details that should be inspected (property name from the Exception class) + /// + public string FieldName { get; set; } + + /// + /// Value that should be matched. + /// + public string Value { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerFilterCondition.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerFilterCondition.cs similarity index 92% rename from src/Server/OneTrueError.Api/Modules/Triggers/TriggerFilterCondition.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/TriggerFilterCondition.cs index 45ee5a69..1abb378b 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerFilterCondition.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerFilterCondition.cs @@ -1,35 +1,35 @@ -using System.ComponentModel; - -namespace OneTrueError.Api.Modules.Triggers -{ - /// - /// Filter condition - /// - public enum TriggerFilterCondition - { - /// - /// Inspected value should start with the filter string - /// - [Description("Starts with")] StartsWith, - - /// - /// Inspected value should end with the filter string - /// - [Description("Ends with")] EndsWith, - - /// - /// Inspected value should contain the filter string - /// - [Description("Contain")] Contains, - - /// - /// Inspected value should not contain the filter string - /// - [Description("Do not contain")] DoNotContain, - - /// - /// Inspected value should equal the filter string (case insensitive) - /// - [Description("Equals")] Equals - } +using System.ComponentModel; + +namespace Coderr.Server.Api.Modules.Triggers +{ + /// + /// Filter condition + /// + public enum TriggerFilterCondition + { + /// + /// Inspected value should start with the filter string + /// + [Description("Starts with")] StartsWith, + + /// + /// Inspected value should end with the filter string + /// + [Description("Ends with")] EndsWith, + + /// + /// Inspected value should contain the filter string + /// + [Description("Contain")] Contains, + + /// + /// Inspected value should not contain the filter string + /// + [Description("Do not contain")] DoNotContain, + + /// + /// Inspected value should equal the filter string (case insensitive) + /// + [Description("Equals")] Equals + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerRuleAction.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerRuleAction.cs similarity index 89% rename from src/Server/OneTrueError.Api/Modules/Triggers/TriggerRuleAction.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/TriggerRuleAction.cs index d023dcc5..642d97cc 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerRuleAction.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerRuleAction.cs @@ -1,23 +1,23 @@ -namespace OneTrueError.Api.Modules.Triggers -{ - /// - /// Action to take when a filter is apssed - /// - public enum TriggerRuleAction - { - /// - /// Do not execute the trigger - /// - AbortTrigger, - - /// - /// Middle manager principle: Lets delegate the decision to the next rule. - /// - ContinueWithNextRule, - - /// - /// Do not check any more rules, just execute the god damn trigger. - /// - ExecuteActions - } +namespace Coderr.Server.Api.Modules.Triggers +{ + /// + /// Action to take when a filter is apssed + /// + public enum TriggerRuleAction + { + /// + /// Do not execute the trigger + /// + AbortTrigger, + + /// + /// Middle manager principle: Lets delegate the decision to the next rule. + /// + ContinueWithNextRule, + + /// + /// Do not check any more rules, just execute the god damn trigger. + /// + ExecuteActions + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerRuleBase.cs b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerRuleBase.cs similarity index 86% rename from src/Server/OneTrueError.Api/Modules/Triggers/TriggerRuleBase.cs rename to src/Server/Coderr.Server.Api/Modules/Triggers/TriggerRuleBase.cs index 3b27a308..05138787 100644 --- a/src/Server/OneTrueError.Api/Modules/Triggers/TriggerRuleBase.cs +++ b/src/Server/Coderr.Server.Api/Modules/Triggers/TriggerRuleBase.cs @@ -1,18 +1,18 @@ -namespace OneTrueError.Api.Modules.Triggers -{ - /// - /// Base class for rules. - /// - public class TriggerRuleBase - { - /// - /// Filter that should be passed - /// - public TriggerFilterCondition Filter { get; set; } - - /// - /// Did we pass the filter? Then do this. - /// - public TriggerRuleAction ResultToUse { get; set; } - } +namespace Coderr.Server.Api.Modules.Triggers +{ + /// + /// Base class for rules. + /// + public class TriggerRuleBase + { + /// + /// Filter that should be passed + /// + public TriggerFilterCondition Filter { get; set; } + + /// + /// Did we pass the filter? Then do this. + /// + public TriggerRuleAction ResultToUse { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersions.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersions.cs new file mode 100644 index 00000000..cc90599e --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersions.cs @@ -0,0 +1,28 @@ +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + /// + /// Get all application versions that we've received incidents for + /// + [Message] + public class GetApplicationVersions : Query + { + /// + /// Creates a new instance of . + /// + /// application to get versions for + /// + public GetApplicationVersions(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + ApplicationId = applicationId; + } + + /// + /// Application id + /// + public int ApplicationId { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersionsResult.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersionsResult.cs new file mode 100644 index 00000000..b261906e --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersionsResult.cs @@ -0,0 +1,13 @@ +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + /// + /// Result for + /// + public class GetApplicationVersionsResult + { + /// + /// All versions + /// + public GetApplicationVersionsResultItem[] Items { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersionsResultItem.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersionsResultItem.cs new file mode 100644 index 00000000..d6b7c725 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetApplicationVersionsResultItem.cs @@ -0,0 +1,35 @@ +using System; + +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + /// + /// Version information + /// + public class GetApplicationVersionsResultItem + { + /// + /// When we received the first incident for this application + /// + public DateTime FirstReportReceivedAtUtc { get; set; } + + /// + /// Number of new incidents + /// + public int IncidentCount { get; set; } + + /// + /// When we received the most recent report for this version + /// + public DateTime LastReportReceivedAtUtc { get; set; } + + /// + /// Number of reports (for old and new incidents) + /// + public int ReportCount { get; set; } + + /// + /// Version string (x.x.x.x) + /// + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistory.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistory.cs new file mode 100644 index 00000000..47b2b771 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistory.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + [Message] + public class GetVersionHistory : Query + { + public int ApplicationId { get; set; } + public DateTime? FromDate { get; set; } + public DateTime? ToDate { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResult.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResult.cs new file mode 100644 index 00000000..4b2f60a8 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResult.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; + +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + [Message] + public class GetVersionHistoryResult + { + /// + /// Year-Month + /// + public string[] Dates { get; set; } + + /// + /// Key = version name, Value = number of incidents for the month + /// + public GetVersionHistoryResultSet[] IncidentCounts { get; set; } + + /// + /// Key = version name, Value = number of error reports for the month + /// + public GetVersionHistoryResultSet[] ReportCounts { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultItem.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultItem.cs new file mode 100644 index 00000000..1f0974e1 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultItem.cs @@ -0,0 +1,12 @@ +using System; + +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + public class GetVersionHistoryResultItem + { + public int IncindentCount { get; set; } + public int ReportCount { get; set; } + public DateTime LastUpdateAtUtc { get; set; } + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultMonth.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultMonth.cs new file mode 100644 index 00000000..15a9ad8b --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultMonth.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + public class GetVersionHistoryResultMonth + { + public DateTime YearMonth { get; set; } + public IList Items { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultSet.cs b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultSet.cs new file mode 100644 index 00000000..c6070030 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Versions/Queries/GetVersionHistoryResultSet.cs @@ -0,0 +1,8 @@ +namespace Coderr.Server.Api.Modules.Versions.Queries +{ + public class GetVersionHistoryResultSet + { + public string Name { get; set; } + public int[] Values { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/AddEntry.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/AddEntry.cs new file mode 100644 index 00000000..172ed4c1 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/AddEntry.cs @@ -0,0 +1,24 @@ +namespace Coderr.Server.Api.Modules.Whitelists.Commands +{ + /// + /// Add a domain that may post error reports without using a shared secret (javascript applications) + /// + [Command] + public class AddEntry + { + /// + /// Applications that the domain is allowed for. + /// + public int[] ApplicationIds { get; set; } = new int[0]; + + /// + /// For instance yourdomain.com. + /// + public string DomainName { get; set; } + + /// + /// To manually specify which IP addresses the domain matches. + /// + public string[] IpAddresses { get; set; } = new string[0]; + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/EditEntry.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/EditEntry.cs new file mode 100644 index 00000000..f39fa81f --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/EditEntry.cs @@ -0,0 +1,25 @@ +namespace Coderr.Server.Api.Modules.Whitelists.Commands +{ + /// + /// Edit a domain that may post error reports without using a shared secret (javascript applications) + /// + [Command] + public class EditEntry + { + /// + /// PK for the entry being edited. + /// + public int Id { get; set; } + + /// + /// Applications that the domain is allowed for. + /// + public int[] ApplicationIds { get; set; } = new int[0]; + + + /// + /// Only manually specified ip addresses. + /// + public string[] IpAddresses { get; set; } = new string[0]; + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/RemoveEntry.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/RemoveEntry.cs new file mode 100644 index 00000000..33d83a6b --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Commands/RemoveEntry.cs @@ -0,0 +1,14 @@ +namespace Coderr.Server.Api.Modules.Whitelists.Commands +{ + /// + /// Remove a previously added white list entry + /// + [Command] + public class RemoveEntry + { + /// + /// Id of the entry. + /// + public int Id { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntries.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntries.cs new file mode 100644 index 00000000..759c4c90 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntries.cs @@ -0,0 +1,21 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Modules.Whitelists.Queries +{ + /// + /// Get whitelist either by application id or DomainName + /// + [Message] + public class GetWhitelistEntries : Query + { + /// + /// Limit result to this application only + /// + public int? ApplicationId { get; set; } + + /// + /// Limit result to this domain name only + /// + public string DomainName { get; set; } + } +} diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResult.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResult.cs new file mode 100644 index 00000000..b6e709ae --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResult.cs @@ -0,0 +1,10 @@ +namespace Coderr.Server.Api.Modules.Whitelists.Queries +{ + /// + /// Result for . + /// + public class GetWhitelistEntriesResult + { + public GetWhitelistEntriesResultItem[] Entries { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItem.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItem.cs new file mode 100644 index 00000000..27b726f2 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItem.cs @@ -0,0 +1,13 @@ +namespace Coderr.Server.Api.Modules.Whitelists.Queries +{ + /// + /// Entry for + /// + public class GetWhitelistEntriesResultItem + { + public int Id { get; set; } + public GetWhitelistEntriesResultItemIp[] IpAddresses { get; set; } + public GetWhitelistEntriesResultItemApp[] Applications { get; set; } + public string DomainName { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItemApp.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItemApp.cs new file mode 100644 index 00000000..b43a9e73 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItemApp.cs @@ -0,0 +1,8 @@ +namespace Coderr.Server.Api.Modules.Whitelists.Queries +{ + public class GetWhitelistEntriesResultItemApp + { + public int ApplicationId { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItemIp.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItemIp.cs new file mode 100644 index 00000000..97845852 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/GetWhitelistEntriesResultItemIp.cs @@ -0,0 +1,11 @@ +using System; + +namespace Coderr.Server.Api.Modules.Whitelists.Queries +{ + public class GetWhitelistEntriesResultItemIp + { + public string Address { get; set; } + public DateTime UpdatedAtUtc { get; set; } + public ResultItemIpType Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/ResultItemIpType.cs b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/ResultItemIpType.cs new file mode 100644 index 00000000..89c4db4d --- /dev/null +++ b/src/Server/Coderr.Server.Api/Modules/Whitelists/Queries/ResultItemIpType.cs @@ -0,0 +1,23 @@ +namespace Coderr.Server.Api.Modules.Whitelists.Queries +{ + /// + /// Typ of stored IP record. + /// + public enum ResultItemIpType + { + /// + /// Added when doing a lookup for the domain + /// + Lookup = 0, + + /// + /// Manually specified by the user + /// + Manual = 1, + + /// + /// We got a request from this IP and a lookup didn't match it. + /// + Denied = 2 + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/NamespaceDoc.cs b/src/Server/Coderr.Server.Api/NamespaceDoc.cs new file mode 100644 index 00000000..e40430e3 --- /dev/null +++ b/src/Server/Coderr.Server.Api/NamespaceDoc.cs @@ -0,0 +1,43 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.Api +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// API + /// + /// + /// The API is based on Command/Queries and events. + /// + /// Commands can be seen as the write model. All operations is done with the + /// help of commands. A command is not an atomic unit, but do in most cases represent an use case. + /// + /// + /// Queries are the read model in the application. They are used to fetch information. Queries are idempotent and + /// may not change + /// application state. + /// + /// + /// Events are used to allow different parts of the application to talk. The publisher are not aware of if there + /// are any + /// subscribers or how many there are. The subscriber have no knowledge about who published the event. + /// + ///

Implementations

+ /// + /// There is a tool in the "Tool" root folder which are used to generate Typescript classes from these + /// APIs. The .ts files can be + /// invoked using Ajax directly from the web. + /// + /// + /// You can also invoke the DTOs directly from your application using a HTTP client. Serialize the DTO as JSON and + /// then include + /// X-Cqs-Object-Type as a HTTP header. It should contain the assembly qualified type name of the DTO. + /// + /// Basic authentication is used. Thus we recommend that you run the site using SSL. + ///
+ [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/ReadMe.md b/src/Server/Coderr.Server.Api/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.Api/ReadMe.md rename to src/Server/Coderr.Server.Api/ReadMe.md diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplication.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplication.cs similarity index 92% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplication.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplication.cs index 42d6c2ab..1008fae1 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplication.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplication.cs @@ -1,34 +1,35 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Get all feedback that is for a specific application - /// - public class GetFeedbackForApplicationPage : Query - { - /// - /// Creates a new instance of . - /// - /// application id - /// applicationId - public GetFeedbackForApplicationPage(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - } - - /// - /// Serialization constructor - /// - protected GetFeedbackForApplicationPage() - { - } - - /// - /// Application id - /// - public int ApplicationId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Get all feedback that is for a specific application + /// + [Message] + public class GetFeedbackForApplicationPage : Query + { + /// + /// Creates a new instance of . + /// + /// application id + /// applicationId + public GetFeedbackForApplicationPage(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + ApplicationId = applicationId; + } + + /// + /// Serialization constructor + /// + protected GetFeedbackForApplicationPage() + { + } + + /// + /// Application id + /// + public int ApplicationId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplicationResult.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplicationResult.cs similarity index 90% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplicationResult.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplicationResult.cs index 59891d3f..3b67f0a5 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplicationResult.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplicationResult.cs @@ -1,26 +1,26 @@ -using System.Collections.Generic; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Result for - /// - public class GetFeedbackForApplicationPageResult - { - /// - /// All emails (included in the first page) - /// - //TODO: crappy solution - public List Emails { get; set; } - - /// - /// items on this page - /// - public GetFeedbackForApplicationPageResultItem[] Items { get; set; } - - /// - /// Total number of items - /// - public int TotalCount { get; set; } - } +using System.Collections.Generic; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Result for + /// + public class GetFeedbackForApplicationPageResult + { + /// + /// All emails (included in the first page) + /// + //TODO: crappy solution + public List Emails { get; set; } + + /// + /// items on this page + /// + public GetFeedbackForApplicationPageResultItem[] Items { get; set; } + + /// + /// Total number of items + /// + public int TotalCount { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplicationResultItem.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplicationResultItem.cs similarity index 92% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplicationResultItem.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplicationResultItem.cs index ef53f4b8..3a978fab 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForApplicationResultItem.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForApplicationResultItem.cs @@ -1,35 +1,35 @@ -using System; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Result item for . - /// - public class GetFeedbackForApplicationPageResultItem - { - /// - /// Email adress to the user (if the user want to get status updates for the incident) - /// - public string EmailAddress { get; set; } - - /// - /// Incident that the feedback belongs to - /// - public int IncidentId { get; set; } - - /// - /// Incident name (typically first line of the exception message) - /// - public string IncidentName { get; set; } - - /// - /// Error description written by the user that experienced the error. - /// - public string Message { get; set; } - - /// - /// When the user wrote the feedback - /// - public DateTime WrittenAtUtc { get; set; } - } +using System; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Result item for . + /// + public class GetFeedbackForApplicationPageResultItem + { + /// + /// Email adress to the user (if the user want to get status updates for the incident) + /// + public string EmailAddress { get; set; } + + /// + /// Incident that the feedback belongs to + /// + public int IncidentId { get; set; } + + /// + /// Incident name (typically first line of the exception message) + /// + public string IncidentName { get; set; } + + /// + /// Error description written by the user that experienced the error. + /// + public string Message { get; set; } + + /// + /// When the user wrote the feedback + /// + public DateTime WrittenAtUtc { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPage.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPage.cs similarity index 77% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPage.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPage.cs index 22ee815f..6d822f32 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPage.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPage.cs @@ -1,11 +1,12 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Get given feedback for all applications. - /// - public class GetFeedbackForDashboardPage : Query - { - } +using DotNetCqs; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Get given feedback for all applications. + /// + [Message] + public class GetFeedbackForDashboardPage : Query + { + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResult.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResult.cs similarity index 90% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResult.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResult.cs index 63910c62..df3881f7 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResult.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResult.cs @@ -1,25 +1,25 @@ -using System.Collections.Generic; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Result for - /// - public class GetFeedbackForDashboardPageResult - { - /// - /// Emails to all users that are waiting on status updates. - /// - public List Emails { get; set; } - - /// - /// Items on the requested page. - /// - public GetFeedbackForDashboardPageResultItem[] Items { get; set; } - - /// - /// Total number of feedback entries - /// - public int TotalCount { get; set; } - } +using System.Collections.Generic; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Result for + /// + public class GetFeedbackForDashboardPageResult + { + /// + /// Emails to all users that are waiting on status updates. + /// + public List Emails { get; set; } + + /// + /// Items on the requested page. + /// + public GetFeedbackForDashboardPageResultItem[] Items { get; set; } + + /// + /// Total number of feedback entries + /// + public int TotalCount { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResultItem.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResultItem.cs similarity index 92% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResultItem.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResultItem.cs index 73a0f331..b3d3914a 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResultItem.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetFeedbackForDashboardPageResultItem.cs @@ -1,36 +1,36 @@ -using System; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Result item for - /// - public class GetFeedbackForDashboardPageResultItem - { - /// - /// Application that the feedback was written for - /// - public int ApplicationId { get; set; } - - /// - /// Name of the application. - /// - public string ApplicationName { get; set; } - - /// - /// Email adress to the user (if the user want to get status updates for the incident) - /// - public string EmailAddress { get; set; } - - - /// - /// Error description written by the user that experienced the error. - /// - public string Message { get; set; } - - /// - /// When the user wrote the feedback - /// - public DateTime WrittenAtUtc { get; set; } - } +using System; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Result item for + /// + public class GetFeedbackForDashboardPageResultItem + { + /// + /// Application that the feedback was written for + /// + public int ApplicationId { get; set; } + + /// + /// Name of the application. + /// + public string ApplicationName { get; set; } + + /// + /// Email adress to the user (if the user want to get status updates for the incident) + /// + public string EmailAddress { get; set; } + + + /// + /// Error description written by the user that experienced the error. + /// + public string Message { get; set; } + + /// + /// When the user wrote the feedback + /// + public DateTime WrittenAtUtc { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedback.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedback.cs similarity index 92% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedback.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedback.cs index b33c8607..662e4b18 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedback.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedback.cs @@ -1,30 +1,31 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Lists all feedback which has been made for an incident - /// - /// - /// Will only fetch for the most specific id - /// - public class GetIncidentFeedback : Query - { - /// - /// Creates a new instance of . - /// - /// Incident to get feedback for - /// incidentId - public GetIncidentFeedback(int incidentId) - { - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - } - - /// - /// Incident to get feedback for - /// - public int IncidentId { get; private set; } - } +using System; +using DotNetCqs; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Lists all feedback which has been made for an incident + /// + /// + /// Will only fetch for the most specific id + /// + [Message] + public class GetIncidentFeedback : Query + { + /// + /// Creates a new instance of . + /// + /// Incident to get feedback for + /// incidentId + public GetIncidentFeedback(int incidentId) + { + if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); + IncidentId = incidentId; + } + + /// + /// Incident to get feedback for + /// + public int IncidentId { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedbackResult.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedbackResult.cs similarity index 94% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedbackResult.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedbackResult.cs index 6e492e48..ccd36901 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedbackResult.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedbackResult.cs @@ -1,43 +1,43 @@ -using System; -using System.Collections.Generic; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Result for . - /// - public class GetIncidentFeedbackResult - { - /// - /// Creates a new instance of . - /// - /// Feedback items - /// Emails to all users that are waiting on status updates. - /// - public GetIncidentFeedbackResult(IReadOnlyList items, ICollection emails) - { - if (items == null) throw new ArgumentNullException("items"); - if (emails == null) throw new ArgumentNullException("emails"); - Items = items; - Emails = emails; - } - - /// - /// Serialization constructor - /// - protected GetIncidentFeedbackResult() - { - Emails = new List(); - } - - /// - /// Emails to all users that are waiting on status updates. - /// - public ICollection Emails { get; set; } - - /// - /// Items - /// - public IReadOnlyList Items { get; private set; } - } +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Result for . + /// + public class GetIncidentFeedbackResult + { + /// + /// Creates a new instance of . + /// + /// Feedback items + /// Emails to all users that are waiting on status updates. + /// + public GetIncidentFeedbackResult(IReadOnlyList items, ICollection emails) + { + if (items == null) throw new ArgumentNullException("items"); + if (emails == null) throw new ArgumentNullException("emails"); + Items = items; + Emails = emails; + } + + /// + /// Serialization constructor + /// + protected GetIncidentFeedbackResult() + { + Emails = new List(); + } + + /// + /// Emails to all users that are waiting on status updates. + /// + public ICollection Emails { get; set; } + + /// + /// Items + /// + public IReadOnlyList Items { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedbackResultItem.cs b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedbackResultItem.cs similarity index 89% rename from src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedbackResultItem.cs rename to src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedbackResultItem.cs index a9b562c7..4f9178fa 100644 --- a/src/Server/OneTrueError.Api/Web/Feedback/Queries/GetIncidentFeedbackResultItem.cs +++ b/src/Server/Coderr.Server.Api/Web/Feedback/Queries/GetIncidentFeedbackResultItem.cs @@ -1,25 +1,25 @@ -using System; - -namespace OneTrueError.Api.Web.Feedback.Queries -{ - /// - /// Result item for . - /// - public class GetIncidentFeedbackResultItem - { - /// - /// Email if user can be contacted. - /// - public string EmailAddress { get; set; } - - /// - /// Error description written by the user that experienced the error - /// - public string Message { get; set; } - - /// - /// When the feedback was written - /// - public DateTime WrittenAtUtc { get; set; } - } +using System; + +namespace Coderr.Server.Api.Web.Feedback.Queries +{ + /// + /// Result item for . + /// + public class GetIncidentFeedbackResultItem + { + /// + /// Email if user can be contacted. + /// + public string EmailAddress { get; set; } + + /// + /// Error description written by the user that experienced the error + /// + public string Message { get; set; } + + /// + /// When the feedback was written + /// + public DateTime WrittenAtUtc { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverview.cs b/src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverview.cs new file mode 100644 index 00000000..40628740 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverview.cs @@ -0,0 +1,30 @@ +using DotNetCqs; + +namespace Coderr.Server.Api.Web.Overview.Queries +{ + /// + /// Get an Coderr summary (typically shown in the chart and right panel summary) + /// + [Message] + public class GetOverview : Query + { + /// + /// Amount of time to look back (i.e. startdate = DateTime.Now.Substract(WindowSize)) + /// + /// + /// 1 = switch to hours + /// + public int NumberOfDays { get; set; } + + + /// + /// Load chart data. + /// + public bool IncludeChartData { get; set; } = true; + + /// + /// Include summary count per partition. + /// + public bool IncludePartitions { get; set; } = false; + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverviewApplicationResult.cs b/src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverviewApplicationResult.cs similarity index 95% rename from src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverviewApplicationResult.cs rename to src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverviewApplicationResult.cs index a050e065..f9978c7d 100644 --- a/src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverviewApplicationResult.cs +++ b/src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverviewApplicationResult.cs @@ -1,81 +1,81 @@ -using System; -using System.Collections.Generic; - -namespace OneTrueError.Api.Web.Overview.Queries -{ - /// - /// Application specific result for - /// - public class GetOverviewApplicationResult - { - private readonly Dictionary _index = new Dictionary(); - - /// - /// Creates a new instance of . - /// - /// Application name - /// first day in sequence - /// Number of days that this result contains - /// label - /// days - public GetOverviewApplicationResult(string label, DateTime startDate, int days) - { - if (label == null) throw new ArgumentNullException("label"); - if (days < 1) throw new ArgumentOutOfRangeException("days"); - - Label = label; - if (days == 1) - { - var startTime = DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-22); - for (var i = 0; i < 24; i++) - { - _index[startTime.AddHours(i)] = i; - } - Values = new int[24]; - } - else - { - for (var i = 0; i < days; i++) - { - _index[startDate.AddDays(i)] = i; - } - Values = new int[days]; - } - } - - /// - /// Serialization constructor - /// - protected GetOverviewApplicationResult() - { - } - - /// - /// Label - /// - public string Label { get; set; } - - /// - /// Values, one per day - /// - public int[] Values { get; set; } - - /// - /// Add another value - /// - /// Date - /// Value - /// value < 0 - public void AddValue(DateTime date, int value) - { - if (value < 0) throw new ArgumentOutOfRangeException("value"); - - //future date = malconfigured reporting clients. - if (!_index.ContainsKey(date)) - return; - - var indexPos = _index[date]; - Values[indexPos] = value; - } - } +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Web.Overview.Queries +{ + /// + /// Application specific result for + /// + public class GetOverviewApplicationResult + { + private readonly Dictionary _index = new Dictionary(); + + /// + /// Creates a new instance of . + /// + /// Application name + /// first day in sequence + /// Number of days that this result contains + /// label + /// days + public GetOverviewApplicationResult(string label, DateTime startDate, int days) + { + if (label == null) throw new ArgumentNullException("label"); + if (days < 1) throw new ArgumentOutOfRangeException("days"); + + Label = label; + if (days == 1) + { + var startTime = DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-22); + for (var i = 0; i < 24; i++) + { + _index[startTime.AddHours(i)] = i; + } + Values = new int[24]; + } + else + { + for (var i = 0; i < days; i++) + { + _index[startDate.AddDays(i)] = i; + } + Values = new int[days]; + } + } + + /// + /// Serialization constructor + /// + protected GetOverviewApplicationResult() + { + } + + /// + /// Label + /// + public string Label { get; set; } + + /// + /// Values, one per day + /// + public int[] Values { get; set; } + + /// + /// Add another value + /// + /// Date + /// Value + /// value < 0 + public void AddValue(DateTime date, int value) + { + if (value < 0) throw new ArgumentOutOfRangeException("value"); + + //future date = malconfigured reporting clients. + if (!_index.ContainsKey(date)) + return; + + var indexPos = _index[date]; + Values[indexPos] = value; + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverviewResult.cs b/src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverviewResult.cs new file mode 100644 index 00000000..031862ca --- /dev/null +++ b/src/Server/Coderr.Server.Api/Web/Overview/Queries/GetOverviewResult.cs @@ -0,0 +1,40 @@ +namespace Coderr.Server.Api.Web.Overview.Queries +{ + /// + /// Result for . + /// + public class GetOverviewResult + { + public GetOverviewResult() + { + IncidentsPerApplication = new GetOverviewApplicationResult[0]; + TimeAxisLabels = new string[0]; + StatSummary = new OverviewStatSummary { }; + } + + /// + /// 1 = switch to hours for incidents and reports. + /// + public int Days { get; set; } + + /// + /// One collection per application + /// + public GetOverviewApplicationResult[] IncidentsPerApplication { get; set; } + + /// + /// Aggregated summary + /// + public OverviewStatSummary StatSummary { get; set; } + + /// + /// Labels for the time axis (X-axis) in the chart. + /// + public string[] TimeAxisLabels { get; set; } + + /// + /// Number of reports that + /// + public int MissedReports { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Web/Overview/Queries/OverviewStatSummary.cs b/src/Server/Coderr.Server.Api/Web/Overview/Queries/OverviewStatSummary.cs new file mode 100644 index 00000000..e36696ad --- /dev/null +++ b/src/Server/Coderr.Server.Api/Web/Overview/Queries/OverviewStatSummary.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace Coderr.Server.Api.Web.Overview.Queries +{ + /// + /// Stats for the last X days, part of . + /// + public class OverviewStatSummary + { + /// + /// Number of followers + /// + public int Followers { get; set; } + + /// + /// Number of incidents + /// + public int Incidents { get; set; } + + /// + /// Number of reports received + /// + public int Reports { get; set; } + + /// + /// Number user feedback items + /// + public int UserFeedback { get; set; } + + public DateTime? NewestIncidentReceivedAtUtc { get; set; } + + public DateTime? NewestReportReceivedAtUtc { get; set; } + + /// + /// Summary per partition + /// + public PartitionOverview[] Partitions { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.Api/Web/Overview/Queries/PartitionOverview.cs b/src/Server/Coderr.Server.Api/Web/Overview/Queries/PartitionOverview.cs new file mode 100644 index 00000000..ed401962 --- /dev/null +++ b/src/Server/Coderr.Server.Api/Web/Overview/Queries/PartitionOverview.cs @@ -0,0 +1,23 @@ +namespace Coderr.Server.Api.Web.Overview.Queries +{ + /// + /// Summary for a partition + /// + public class PartitionOverview + { + /// + /// Name, used when reporting errors. + /// + public string Name { get; set; } + + /// + /// Name to show for users. + /// + public string DisplayName { get; set; } + + /// + /// Amount of unique values for this partition. + /// + public int Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/App.config b/src/Server/Coderr.Server.App.Tests/App.config similarity index 100% rename from src/Server/OneTrueError.App.Tests/App.config rename to src/Server/Coderr.Server.App.Tests/App.config diff --git a/src/Server/Coderr.Server.App.Tests/Coderr.Server.App.Tests.csproj b/src/Server/Coderr.Server.App.Tests/Coderr.Server.App.Tests.csproj new file mode 100644 index 00000000..0ab70ef6 --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Coderr.Server.App.Tests.csproj @@ -0,0 +1,29 @@ + + + netcoreapp3.1 + Coderr.Server.App.Tests + Coderr.Server.App.Tests + Debug;Release;Premise + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/ConfigWrapper.cs b/src/Server/Coderr.Server.App.Tests/ConfigWrapper.cs new file mode 100644 index 00000000..0406fb70 --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/ConfigWrapper.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using Coderr.Server.Abstractions.Config; + +namespace Coderr.Server.App.Tests +{ + public class ConfigWrapper : IConfiguration + where TConfigType : IConfigurationSection, new() + { + private readonly ConfigurationStore _configurationStore; + private TConfigType _value; + + public ConfigWrapper(ConfigurationStore configurationStore) + { + _configurationStore = configurationStore; + } + + public TConfigType Value + { + get + { + if (EqualityComparer.Default.Equals(_value, default(TConfigType))) + _value = _configurationStore.Load(); + + return _value; + } + } + + public void Save() + { + _configurationStore.Store(Value); + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Configuration/ConfigurationCategoryExtensionsTests.cs b/src/Server/Coderr.Server.App.Tests/Configuration/ConfigurationCategoryExtensionsTests.cs similarity index 80% rename from src/Server/OneTrueError.App.Tests/Configuration/ConfigurationCategoryExtensionsTests.cs rename to src/Server/Coderr.Server.App.Tests/Configuration/ConfigurationCategoryExtensionsTests.cs index ffc5d7d9..d071ca56 100644 --- a/src/Server/OneTrueError.App.Tests/Configuration/ConfigurationCategoryExtensionsTests.cs +++ b/src/Server/Coderr.Server.App.Tests/Configuration/ConfigurationCategoryExtensionsTests.cs @@ -1,62 +1,62 @@ -using System.Globalization; -using System.Threading; -using FluentAssertions; -using OneTrueError.App.Tests.Configuration.TestEntitites; -using OneTrueError.Infrastructure.Configuration; -using Xunit; - -namespace OneTrueError.App.Tests.Configuration -{ - public class ConfigurationCategoryExtensionsTests - { - [Fact] - public void do_not_persist_category_name_as_its_just_metadata_and_part_of_the_key() - { - var cat = new SoCultural(); - cat.Number = 43.32f; - - Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-se"); - var dict = cat.ToConfigDictionary(); - - dict.Keys.Should().NotContain("SectionName"); - } - - [Fact] - public void should_be_able_to_work_despite_local_culture_when_persisting_configuration() - { - var cat = new SoCultural(); - cat.Number = 43.32f; - - Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-se"); - var dict = cat.ToConfigDictionary(); - - dict["Number"].Should().Be("43.32"); - 43.32f.ToString() - .Should() - .Be("43,32", "because we need to validate changes when another culture is active."); - } - - [Fact] - public void should_format_values_with_invariant_culture() - { - var cat = new SoCultural(); - cat.Number = 43.32f; - - Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-se"); - var dict = cat.ToConfigDictionary(); - - dict["Number"].Should().Be("43.32"); - 43.32f.ToString().Should().Be("43,32", "because we need to validate that the config is correct."); - } - - [Fact] - public void should_ignore_categoryName_when_generating_The_dictionary() - { - var cat = new WriteTestSection(); - - var dict = cat.ToConfigDictionary(); - - dict.Keys.Should().NotContain(x => x == "SectionName"); - } - } +using System.Globalization; +using System.Threading; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.App.Tests.Configuration.TestEntitites; +using FluentAssertions; +using Xunit; + +namespace Coderr.Server.App.Tests.Configuration +{ + public class ConfigurationCategoryExtensionsTests + { + [Fact] + public void do_not_persist_category_name_as_its_just_metadata_and_part_of_the_key() + { + var cat = new SoCultural(); + cat.Number = 43.32f; + + Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-se"); + var dict = cat.ToConfigDictionary(); + + dict.Keys.Should().NotContain("SectionName"); + } + + [Fact] + public void Should_be_able_to_work_despite_local_culture_when_persisting_configuration() + { + var cat = new SoCultural(); + cat.Number = 43.32f; + + Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-se"); + var dict = cat.ToConfigDictionary(); + + dict["Number"].Should().Be("43.32"); + 43.32f.ToString() + .Should() + .Be("43,32", "because we need to validate changes when another culture is active."); + } + + [Fact] + public void Should_format_values_with_invariant_culture() + { + var cat = new SoCultural(); + cat.Number = 43.32f; + + Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-se"); + var dict = cat.ToConfigDictionary(); + + dict["Number"].Should().Be("43.32"); + 43.32f.ToString().Should().Be("43,32", "because we need to validate that the config is correct."); + } + + [Fact] + public void Should_ignore_categoryName_when_generating_The_dictionary() + { + var cat = new WriteTestSection(); + + var dict = cat.ToConfigDictionary(); + + dict.Keys.Should().NotContain(x => x == "SectionName"); + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/Configuration/DictionaryExtensionsTests.cs b/src/Server/Coderr.Server.App.Tests/Configuration/DictionaryExtensionsTests.cs new file mode 100644 index 00000000..4fda6036 --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Configuration/DictionaryExtensionsTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.Infrastructure.Configuration; +using FluentAssertions; +using Xunit; + +namespace Coderr.Server.App.Tests.Configuration +{ + public class DictionaryExtensionsTests + { + #region strings + + [Fact] + public void Should_return_value_if_given_key_exists() + { + var dict = new Dictionary {{"Name", "Vlue"}}; + + var actual = dict.GetString("Name"); + + actual.Should().Be("Vlue"); + } + + [Fact] + public void Should_return_default_Value_if_given_key_do_not_exist() + { + var dict = new Dictionary {{"Name", "Vlue"}}; + + var actual = dict.GetString("Supplier", "Santa"); + + actual.Should().Be("Santa"); + } + + #endregion + + #region boolean + + [Fact] + public void Should_include_key_name_if_item_do_not_exist() + { + var dict = new Dictionary {{"Usable", "Vlue"}}; + + Action actual = () => dict.GetBoolean("hey!", null); + + actual.Should().Throw().Which.Message.Contains("hey!"); + } + + [Fact] + public void Should_convert_to_bolean_if_given_item_is_found() + { + var dict = new Dictionary {{"Usable", "True"}}; + + var actual = dict.GetBoolean("Usable"); + + actual.Should().BeTrue(); + } + + + [Fact] + public void Should_include_value_if_item_is_not_convertable_to_boolean() + { + var dict = new Dictionary {{"Usable", "Vlue"}}; + + Action actual = () => dict.GetBoolean("Usable"); + + actual.Should().Throw().Which.Message.Contains("Vlue"); + } + + #endregion + + #region integer + + [Fact] + public void Should_include_key_name_if_int_item_do_not_exist() + { + var dict = new Dictionary {{"Length", "Vlue"}}; + + Action actual = () => dict.GetInteger("hey!", null); + + actual.Should().Throw().Which.Message.Contains("hey!"); + } + + [Fact] + public void Should_convert_to_integer_if_given_item_is_found() + { + var dict = new Dictionary {{"Length", "1100"}}; + + var actual = dict.GetInteger("Length"); + + actual.Should().Be(1100); + } + + + [Fact] + public void Should_include_value_if_item_is_not_convertable_to_integer() + { + var dict = new Dictionary {{"Usable", "Vlue"}}; + + Action actual = () => dict.GetInteger("Usable"); + + actual.Should().Throw().Which.Message.Contains("Vlue"); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/MySection.cs b/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/MySection.cs similarity index 83% rename from src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/MySection.cs rename to src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/MySection.cs index 70ab7433..6c028d8f 100644 --- a/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/MySection.cs +++ b/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/MySection.cs @@ -1,28 +1,28 @@ -using System.Collections.Generic; -using FluentAssertions.Execution; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Tests.Configuration.TestEntitites -{ - internal class MySection : IConfigurationSection - { - public string Name { get; private set; } - - public string SectionName - { - get { return "DemoCategory"; } - } - - public IDictionary ToDictionary() - { - return new Dictionary {{"Name", Name}}; - } - - public void Load(IDictionary settings) - { - Name = settings["Name"]; - if (settings.Count != 1) - throw new AssertionFailedException("Expected only one setting."); - } - } +using System.Collections.Generic; +using Coderr.Server.Abstractions.Config; +using FluentAssertions.Execution; + +namespace Coderr.Server.App.Tests.Configuration.TestEntitites +{ + internal class MySection : IConfigurationSection + { + public string Name { get; private set; } + + public string SectionName + { + get { return "DemoCategory"; } + } + + public IDictionary ToDictionary() + { + return new Dictionary {{"Name", Name}}; + } + + public void Load(IDictionary settings) + { + Name = settings["Name"]; + if (settings.Count != 1) + throw new AssertionFailedException("Expected only one setting."); + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/NonExistantSection.cs b/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/NonExistantSection.cs new file mode 100644 index 00000000..3303bc96 --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/NonExistantSection.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Infrastructure.Configuration; + +namespace Coderr.Server.App.Tests.Configuration.TestEntitites +{ + public class NonExistantSection : IConfigurationSection + { + public string SectionName + { + get { return "NpNp"; } + } + + public IDictionary ToDictionary() + { + return this.ToConfigDictionary(); + } + + public void Load(IDictionary settings) + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/SoCultural.cs b/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/SoCultural.cs new file mode 100644 index 00000000..61aec782 --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/SoCultural.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Infrastructure.Configuration; + +namespace Coderr.Server.App.Tests.Configuration.TestEntitites +{ + internal class SoCultural : IConfigurationSection + { + public float Number { get; set; } + + public string SectionName + { + get { return "SoCultural"; } + } + + public IDictionary ToDictionary() + { + return this.ToConfigDictionary(); + } + + public void Load(IDictionary settings) + { + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/WriteTestSection.cs b/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/WriteTestSection.cs similarity index 81% rename from src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/WriteTestSection.cs rename to src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/WriteTestSection.cs index c8ea3b54..0063db9f 100644 --- a/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/WriteTestSection.cs +++ b/src/Server/Coderr.Server.App.Tests/Configuration/TestEntitites/WriteTestSection.cs @@ -1,30 +1,30 @@ -using System.Collections.Generic; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Tests.Configuration.TestEntitites -{ - internal class WriteTestSection : IConfigurationSection - { - public WriteTestSection() - { - Properties = new Dictionary(); - } - - public IDictionary Properties { get; private set; } - - public string SectionName - { - get { return "WriteTest"; } - } - - public IDictionary ToDictionary() - { - return Properties; - } - - public void Load(IDictionary settings) - { - Properties = settings; - } - } +using System.Collections.Generic; +using Coderr.Server.Abstractions.Config; + +namespace Coderr.Server.App.Tests.Configuration.TestEntitites +{ + internal class WriteTestSection : IConfigurationSection + { + public WriteTestSection() + { + Properties = new Dictionary(); + } + + public IDictionary Properties { get; private set; } + + public string SectionName + { + get { return "WriteTest"; } + } + + public IDictionary ToDictionary() + { + return Properties; + } + + public void Load(IDictionary settings) + { + Properties = settings; + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Core/Accounts/AccountTests.cs b/src/Server/Coderr.Server.App.Tests/Core/Accounts/AccountTests.cs similarity index 77% rename from src/Server/OneTrueError.App.Tests/Core/Accounts/AccountTests.cs rename to src/Server/Coderr.Server.App.Tests/Core/Accounts/AccountTests.cs index fb9a68e9..d8624980 100644 --- a/src/Server/OneTrueError.App.Tests/Core/Accounts/AccountTests.cs +++ b/src/Server/Coderr.Server.App.Tests/Core/Accounts/AccountTests.cs @@ -1,100 +1,100 @@ -using System; -using System.Security.Authentication; -using FluentAssertions; -using OneTrueError.App.Core.Accounts; -using Xunit; - -namespace OneTrueError.App.Tests.Core.Accounts -{ - public class AccountTests - { - [Fact] - public void should_be_able_to_activate_the_account() - { - var sut = new Account("arne", "kalle"); - - sut.Activate(); - var actual = sut.Login("kalle"); - - actual.Should().BeTrue(); - } - - [Fact] - public void should_be_able_to_change_password() - { - var sut = new Account("arne", "kalle"); - - sut.ChangePassword("new"); - var actual = sut.Login("new"); - - actual.Should().BeTrue(); - } - - [Fact] - public void should_be_able_to_validate_the_current_password_so_that_We_know_that_its_safe_to_change_it() - { - var sut = new Account("arne", "kalle"); - - var actual = sut.ValidatePassword("kalle"); - - actual.Should().BeTrue(); - } - - [Fact] - public void should_fail_if_account_have_not_been_activated() - { - var sut = new Account("arne", "kalle"); - - Action actual = () => sut.Login("ping"); - - actual.ShouldThrow(); - } - - [Fact] - public void - should_generate_activation_key_when_reset_is_requested_so_that_the_user_can_activate_its_account_user_the_emailed_link - () - { - var sut = new Account("arne", "kalle"); - sut.Activate(); - sut.Login("ping"); - sut.Login("pong"); - try - { - sut.Login("ping"); - } - catch - { - } - - sut.RequestPasswordReset(); - - sut.ActivationKey.Should().NotBeNull(); - sut.AccountState.Should().Be(AccountState.ResetPassword); - } - - [Fact] - public void three_failed_login_attempts_should_lock_the_account() - { - var sut = new Account("arne", "kalle"); - sut.Activate(); - - sut.Login("ping"); - sut.Login("pong"); - Action actual = () => sut.Login("ping"); - - actual.ShouldThrow(); - sut.AccountState.Should().Be(AccountState.Locked); - } - - [Fact] - public void validate_should_not_accept_invalid_password() - { - var sut = new Account("arne", "kalle"); - - var actual = sut.ValidatePassword("kall"); - - actual.Should().BeFalse(); - } - } +using System; +using System.Security.Authentication; +using Coderr.Server.Domain.Core.Account; +using FluentAssertions; +using Xunit; + +namespace Coderr.Server.App.Tests.Core.Accounts +{ + public class AccountTests + { + [Fact] + public void Should_be_able_to_activate_the_account() + { + var sut = new Account("arne", "kalle"); + + sut.Activate(); + var actual = sut.Login("kalle"); + + actual.Should().BeTrue(); + } + + [Fact] + public void Should_be_able_to_change_password() + { + var sut = new Account("arne", "kalle"); + + sut.ChangePassword("new"); + var actual = sut.Login("new"); + + actual.Should().BeTrue(); + } + + [Fact] + public void Should_be_able_to_validate_the_current_password_so_that_We_know_that_its_safe_to_change_it() + { + var sut = new Account("arne", "kalle"); + + var actual = sut.ValidatePassword("kalle"); + + actual.Should().BeTrue(); + } + + [Fact] + public void Should_fail_if_account_have_not_been_activated() + { + var sut = new Account("arne", "kalle"); + + Action actual = () => sut.Login("ping"); + + actual.Should().Throw(); + } + + [Fact] + public void + Should_generate_activation_key_when_reset_is_requested_so_that_the_user_can_activate_its_account_user_the_emailed_link + () + { + var sut = new Account("arne", "kalle"); + sut.Activate(); + sut.Login("ping"); + sut.Login("pong"); + try + { + sut.Login("ping"); + } + catch + { + } + + sut.RequestPasswordReset(); + + sut.ActivationKey.Should().NotBeNull(); + sut.AccountState.Should().Be(AccountState.ResetPassword); + } + + [Fact] + public void three_failed_login_attempts_should_lock_the_account() + { + var sut = new Account("arne", "kalle"); + sut.Activate(); + + sut.Login("ping"); + sut.Login("pong"); + Action actual = () => sut.Login("ping"); + + actual.Should().Throw(); + sut.AccountState.Should().Be(AccountState.Locked); + } + + [Fact] + public void validate_should_not_accept_invalid_password() + { + var sut = new Account("arne", "kalle"); + + var actual = sut.ValidatePassword("kall"); + + actual.Should().BeFalse(); + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/Core/Accounts/CommandHandlers/RegisterAccountHandlerTests.cs b/src/Server/Coderr.Server.App.Tests/Core/Accounts/CommandHandlers/RegisterAccountHandlerTests.cs new file mode 100644 index 00000000..88a71545 --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Core/Accounts/CommandHandlers/RegisterAccountHandlerTests.cs @@ -0,0 +1,67 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Commands; +using Coderr.Server.Api.Core.Accounts.Events; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.App.Core.Accounts.CommandHandlers; +using Coderr.Server.Domain.Core.Account; +using DotNetCqs; +using FluentAssertions; +using NSubstitute; +using Xunit; + +namespace Coderr.Server.App.Tests.Core.Accounts.CommandHandlers +{ + public class RegisterAccountHandlerTests + { + [Fact] + public async Task Should_create_a_new_account() + { + var configStore = new TestStore(); + var repos = Substitute.For(); + var cmd = new RegisterAccount("rne", "yo", "some@Emal.com"); + var context = Substitute.For(); + repos.When(x => x.CreateAsync(Arg.Any())) + .Do(x => x.Arg().SetId(3)); + + + var sut = new RegisterAccountHandler(repos, configStore); + await sut.HandleAsync(context, cmd); + await repos.Received().CreateAsync(Arg.Any()); + } + + [Fact] + public async Task Should_inform_the_rest_of_the_system_about_the_new_account() + { + var configStore = new TestStore(); + var repos = Substitute.For(); + var context = Substitute.For(); + var cmd = new RegisterAccount("rne", "yo", "some@Emal.com"); + repos.When(x => x.CreateAsync(Arg.Any())) + .Do(x => x.Arg().SetId(3)); + + + var sut = new RegisterAccountHandler(repos, configStore); + await sut.HandleAsync(context, cmd); + + await context.Received().SendAsync(Arg.Any()); + context.Method("SendAsync").Arg().AccountId.Should().Be(3); + } + + [Fact] + public async Task Should_send_activation_email() + { + var configStore = new TestStore(); + var repos = Substitute.For(); + var context = Substitute.For(); + var cmd = new RegisterAccount("rne", "yo", "some@Emal.com"); + repos.When(x => x.CreateAsync(Arg.Any())) + .Do(x => x.Arg().SetId(3)); + + + var sut = new RegisterAccountHandler(repos, configStore); + await sut.HandleAsync(context, cmd); + + await context.Received().SendAsync(Arg.Any()); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/Core/ApiKeys/ApiKeyTests.cs b/src/Server/Coderr.Server.App.Tests/Core/ApiKeys/ApiKeyTests.cs new file mode 100644 index 00000000..40925d1a --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Core/ApiKeys/ApiKeyTests.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.App.Core.ApiKeys; +using Coderr.Server.Infrastructure.Security; +using FluentAssertions; +using Xunit; + +namespace Coderr.Server.App.Tests.Core.ApiKeys +{ + public class ApiKeyTests + { + [Fact] + public void added_application_should_be_added_into_the_claim_list() + { + var sut = new ApiKey(); + + sut.Add(1); + + var claim = sut.Claims.FirstOrDefault(x => x.Type == CoderrClaims.Application && x.Value == "1"); + claim.Should().NotBeNull("because applications Should be represented as claims"); + } + + } +} diff --git a/src/Server/Coderr.Server.App.Tests/Core/Applications/Commands/InviteUserHandlerTests.cs b/src/Server/Coderr.Server.App.Tests/Core/Applications/Commands/InviteUserHandlerTests.cs new file mode 100644 index 00000000..45ff80ea --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Core/Applications/Commands/InviteUserHandlerTests.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Security; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Applications.Events; +using Coderr.Server.Api.Core.Invitations.Commands; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.App.Core.Invitations; +using Coderr.Server.App.Core.Invitations.CommandHandlers; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Domain.Core.User; +using Coderr.Server.Infrastructure.Configuration; +using Coderr.Server.Infrastructure.Security; +using DotNetCqs; +using FluentAssertions; +using NSubstitute; +using Xunit; + +namespace Coderr.Server.App.Tests.Core.Applications.Commands +{ + public class InviteUserHandlerTests + { + private readonly IApplicationRepository _applicationRepository; + private readonly IInvitationRepository _invitationRepository; + private readonly InviteUserHandler _sut; + private readonly IUserRepository _userRepository; + private readonly IMessageContext _context; + private readonly TestStore _configStore; + + public InviteUserHandlerTests() + { + _invitationRepository = Substitute.For(); + _userRepository = Substitute.For(); + _applicationRepository = Substitute.For(); + _userRepository.GetUserAsync(1).Returns(new User(1, "First")); + _applicationRepository.GetByIdAsync(1).Returns(new Application(1, "MyApp")); + _context = Substitute.For(); + _configStore = new TestStore(); + _sut = new InviteUserHandler(_invitationRepository, _userRepository, _applicationRepository, new ConfigWrapper(_configStore)); + } + + [Fact] + public async Task Should_create_an_invite_for_a_new_user() + { + var cmd = new InviteUser(1, "jonas@gauffin.com") { UserId = 1 }; + var members = new[] { new ApplicationTeamMember(1, 3, "karl") }; + ApplicationTeamMember actual = null; + _applicationRepository.GetTeamMembersAsync(1).Returns(members); + _applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any())) + .Do(x => actual = x.Arg()); + _context.Principal.Returns(CreateAdminPrincipal()); + + await _sut.HandleAsync(_context, cmd); + + await _applicationRepository.Received().CreateAsync(Arg.Any()); + actual.EmailAddress.Should().Be(cmd.EmailAddress); + actual.ApplicationId.Should().Be(cmd.ApplicationId); + actual.AddedAtUtc.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); + actual.AddedByName.Should().Be("First"); + } + + [Fact] + public async Task Regular_user_should_not_be_able_to_invite() + { + var cmd = new InviteUser(1, "jonas@gauffin.com") { UserId = 1 }; + var members = new[] { new ApplicationTeamMember(1, 3, "karl") }; + _applicationRepository.GetTeamMembersAsync(1).Returns(members); + _context.Principal.Returns(CreateUserPrincipal()); + + Func actual = async () => await _sut.HandleAsync(_context, cmd); + + await actual.Should().ThrowAsync(); + } + + [Fact] + public async Task Sysadmin_should_be_able_To_invite_users() + { + var cmd = new InviteUser(1, "jonas@gauffin.com") { UserId = 1 }; + var members = new[] { new ApplicationTeamMember(3, 3, "karl") }; + _applicationRepository.GetTeamMembersAsync(1).Returns(members); + _context.Principal.Returns(CreateAdminPrincipal()); + + await _sut.HandleAsync(_context, cmd); + + await _context.Received().SendAsync(Arg.Any()); + } + + [Fact] + public async Task Should_not_allow_invites_when_the_invited_user_already_have_an_account() + { + var cmd = new InviteUser(1, "jonas@gauffin.com") { UserId = 2 }; + var members = new[] { new ApplicationTeamMember(1, 3, "karl") }; + _userRepository.FindByEmailAsync(cmd.EmailAddress).Returns(new User(3, "existing")); + _applicationRepository.GetTeamMembersAsync(1).Returns(members); + _context.Principal.Returns(CreateAdminPrincipal()); + + await _sut.HandleAsync(_context, cmd); + + await _applicationRepository.DidNotReceive().CreateAsync(Arg.Any()); + } + + [Fact] + public async Task Should_not_allow_invites_when_the_invited_user_already_have_an_pending_invite() + { + var cmd = new InviteUser(1, "jonas@gauffin.com") { UserId = 1 }; + var members = new[] { new ApplicationTeamMember(1, cmd.EmailAddress) }; + _applicationRepository.GetTeamMembersAsync(1).Returns(members); + _context.Principal.Returns(CreateAdminPrincipal()); + + await _sut.HandleAsync(_context, cmd); + + await _applicationRepository.DidNotReceive().CreateAsync(Arg.Any()); + } + + [Fact] + public async Task Should_notify_the_system_of_the_invite() + { + var cmd = new InviteUser(1, "jonas@gauffin.com") { UserId = 1 }; + var members = new[] { new ApplicationTeamMember(1, 3, "karl") }; + ApplicationTeamMember actual = null; + _applicationRepository.GetTeamMembersAsync(1).Returns(members); + _applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any())) + .Do(x => actual = x.Arg()); + _context.Principal.Returns(CreateAdminPrincipal()); + + await _sut.HandleAsync(_context, cmd); + + await _applicationRepository.Received().CreateAsync(Arg.Any()); + await _context.Received().SendAsync(Arg.Any()); + } + + [Fact] + public async Task Should_send_an_invitation_email() + { + var cmd = new InviteUser(1, "jonas@gauffin.com") { UserId = 1 }; + var members = new[] { new ApplicationTeamMember(1, 3, "karl") }; + ApplicationTeamMember actual = null; + _applicationRepository.GetTeamMembersAsync(1).Returns(members); + _applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any())) + .Do(x => actual = x.Arg()); + _context.Principal.Returns(CreateAdminPrincipal()); + + await _sut.HandleAsync(_context, cmd); + + await _context.Received().SendAsync(Arg.Any()); + } + + private ClaimsPrincipal CreateAdminPrincipal() + { + var claims = new List + { + new Claim(ClaimTypes.Role, CoderrRoles.SysAdmin), + new Claim(CoderrClaims.ApplicationAdmin, "1") + }; + var identity = new ClaimsIdentity(claims, AuthenticationTypes.Default); + return new ClaimsPrincipal(identity); + } + + private ClaimsPrincipal CreateUserPrincipal() + { + var claims = new List + { + }; + var identity = new ClaimsIdentity(claims, AuthenticationTypes.Default); + return new ClaimsPrincipal(identity); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/Core/Invitations/Commands/InvitationServiceTests.cs b/src/Server/Coderr.Server.App.Tests/Core/Invitations/Commands/InvitationServiceTests.cs new file mode 100644 index 00000000..69c9cc29 --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Core/Invitations/Commands/InvitationServiceTests.cs @@ -0,0 +1,151 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Events; +using Coderr.Server.Api.Core.Accounts.Requests; +using Coderr.Server.App.Core.Accounts; +using Coderr.Server.App.Core.Invitations; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; +using FluentAssertions; +using NSubstitute; +using Xunit; + +namespace Coderr.Server.App.Tests.Core.Invitations.Commands +{ + public class InvitationServiceTests + { + public InvitationServiceTests() + { + _repository = Substitute.For(); + _accountRepository = Substitute.For(); + _applicationRepository = Substitute.For(); + _messageBus = Substitute.For(); + _sut = new AccountService(_accountRepository, _messageBus, _applicationRepository, _repository); + _invitedAccount = new Account("arne", "1234"); + _invitedAccount.SetId(InvitedAccountId); + _invitedAccount.SetVerifiedEmail("jonas@gauffin.com"); + _accountRepository.GetByIdAsync(InvitedAccountId).Returns(_invitedAccount); + } + + private readonly IInvitationRepository _repository; + private readonly IAccountRepository _accountRepository; + private readonly AccountService _sut; + private readonly IMessageBus _messageBus; + private readonly Account _invitedAccount; + private readonly IApplicationRepository _applicationRepository; + private const int InvitedAccountId = 999; + + [Fact] + public async Task + Should_delete_invitation_when_its_accepted_to_prevent_creating_multiple_accounts_with_the_same_invitation_key() + { + var invitation = new Invitation("invited@test.com", "inviter"); + var request = + new AcceptInvitation(InvitedAccountId, invitation.InvitationKey) {AcceptedEmail = "arne@gauffin.com"}; + invitation.Add(1, "arne"); + _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); + var principal = PrincipalHelper.Create(52, "arne"); + + var actual = await _sut.AcceptInvitation(principal, request); + + actual.Should().NotBeNull(); + } + + [Fact] + public async Task Should_notify_system_of_the_accepted_invitation() + { + var invitation = new Invitation("invited@test.com", "inviter"); + var request = + new AcceptInvitation(InvitedAccountId, invitation.InvitationKey) {AcceptedEmail = "arne@gauffin.com"}; + invitation.Add(1, "arne"); + _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); + var principal = PrincipalHelper.Create(52, "arne"); + + var actual = await _sut.AcceptInvitation(principal, request); + + await _messageBus.Received().SendAsync(principal, Arg.Any()); + var evt = _messageBus.Method("SendAsync").Arg(); + evt.AcceptedEmailAddress.Should().Be(request.AcceptedEmail); + evt.AccountId.Should().Be(InvitedAccountId); + evt.ApplicationIds[0].Should().Be(1); + evt.UserName.Should().Be(_invitedAccount.UserName); + } + + [Fact] + public async Task Should_create_an_Account_for_invites_to_new_users() + { + var invitation = new Invitation("invited@test.com", "inviter"); + var request = + new AcceptInvitation("arne", "pass", invitation.InvitationKey) {AcceptedEmail = "arne@gauffin.com"}; + invitation.Add(1, "arne"); + _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); + _accountRepository + .WhenForAnyArgs(x => x.CreateAsync(null)) + .Do(x => x.Arg().SetId(52)); + var principal = PrincipalHelper.Create(52, "arne"); + + + var actual = await _sut.AcceptInvitation(principal, request); + + await _accountRepository.Received().CreateAsync(Arg.Any()); + var evt = _messageBus.Method("SendAsync").Arg(); + evt.AccountId.Should().Be(52); + } + + [Fact] + public async Task + Should_publish_AccountRegistered_if_a_new_account_is_created_as_we_bypass_the_regular_account_registration_flow() + { + var invitation = new Invitation("invited@test.com", "inviter"); + var request = + new AcceptInvitation("arne", "pass", invitation.InvitationKey) {AcceptedEmail = "arne@gauffin.com"}; + invitation.Add(1, "arne"); + _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); + _accountRepository + .WhenForAnyArgs(x => x.CreateAsync(null)) + .Do(x => x.Arg().SetId(52)); + var principal = PrincipalHelper.Create(52, "arne"); + + + var actual = await _sut.AcceptInvitation(principal, request); + + await _messageBus.Received().SendAsync(principal, Arg.Any()); + var evt = _messageBus.Method("SendAsync").Arg(); + evt.AccountId.Should().Be(52); + } + + [Fact] + public async Task + Should_publish_AccountActivated_if_a_new_account_is_created_as_we_bypass_the_regular_account_registration_flow() + { + var invitation = new Invitation("invited@test.com", "inviter"); + var request = + new AcceptInvitation("arne", "pass", invitation.InvitationKey) {AcceptedEmail = "arne@gauffin.com"}; + invitation.Add(1, "arne"); + _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); + _accountRepository + .WhenForAnyArgs(x => x.CreateAsync(null)) + .Do(x => x.Arg().SetId(52)); + var principal = PrincipalHelper.Create(52, "arne"); + + + var actual = await _sut.AcceptInvitation(principal, request); + + await _messageBus.Received().SendAsync(principal, Arg.Any()); + var evt = _messageBus.Method("SendAsync").Arg(); + evt.AccountId.Should().Be(52); + } + + + [Fact] + public async Task Should_ignore_invitations_where_the_key_is_not_registered_in_the_db() + { + var request = new AcceptInvitation(InvitedAccountId, "invalid") {AcceptedEmail = "arne@gauffin.com"}; + var principal = PrincipalHelper.Create(52, "arne"); + + var actual = await _sut.AcceptInvitation(principal, request); + + actual.Should().BeNull(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/NSubstituteExtensions.cs b/src/Server/Coderr.Server.App.Tests/NSubstituteExtensions.cs new file mode 100644 index 00000000..ec60947e --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/NSubstituteExtensions.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NSubstitute; +using NSubstitute.Core; + +namespace Coderr.Server.App.Tests +{ + internal static class NSubstituteExtensions + { + public static MethodListWrapper Method(this object instance, string methodName) + { + var calls = instance.ReceivedCalls().Where(x => x.GetMethodInfo().Name == methodName).ToList(); + return new MethodListWrapper(instance, calls); + } + + public static MethodWrapper Method(this object instance, string methodName, int indexer) + { + var calls = instance.ReceivedCalls().Where(x => x.GetMethodInfo().Name == methodName).ToList(); + if (calls.Count <= indexer) + throw new InvalidOperationException("There are only " + calls.Count + " calls available, you specified index " + indexer); + + return new MethodWrapper(instance, calls[0]); + } + } + + internal class MethodWrapper + { + private readonly ICall _call; + private readonly object _instance; + + public MethodWrapper(object instance, ICall call) + { + _instance = instance; + _call = call; + } + + public TArgument Arg(int index) + { + if ((index < 0) || (index >= _call.GetArguments().Length)) + throw new InvalidOperationException("Method '" + _call.GetMethodInfo().Name + + "' do not have that many arguments."); + + if (_call.GetArguments()[index] is TArgument) + return (TArgument) _call.GetArguments()[index]; + + throw new InvalidOperationException("Argument " + index + " of '" + _call.GetMethodInfo().Name + + "' cannot be converted to '" + typeof(TArgument) + "'."); + } + + public TArgument Arg() + { + var args = _call.GetArguments().Where(x => x is TArgument).ToList(); + if (args.Count != 1) + throw new InvalidOperationException("More than one argument of type '" + _call.GetMethodInfo().Name + + "' is of type " + typeof(TArgument)); + + return (TArgument) args[0]; + } + } + + internal class MethodListWrapper + { + private readonly IList _calls; + private readonly object _instance; + + public MethodListWrapper(object instance, IList calls) + { + _instance = instance; + _calls = calls; + } + + public TArgument Arg(int index) + { + List values = new List(); + foreach (var call in _calls) + { + if ((index < 0) || (index >= call.GetArguments().Length)) + continue; + + if (call.GetArguments()[index] is TArgument) + values.Add(call.GetArguments()[index]); + } + if (values.Count > 1) + throw new InvalidOperationException("There was multiple calls to "+ _calls.First().GetMethodInfo().Name + " with an argument of type "+ typeof(TArgument) + ". Use method call indexer."); + if (values.Count == 1) + return (TArgument) values[0]; + + throw new InvalidOperationException("None of the calls to '" + _calls.First().GetMethodInfo().Name + "' had an argument of type " + typeof(TArgument)); + } + + public TArgument Arg() + { + List values = new List(); + foreach (var call in _calls) + { + var arg = call.GetArguments().FirstOrDefault(x => x.GetType() == typeof(TArgument)); + if (arg != null) + values.Add(arg); + } + if (values.Count > 1) + throw new InvalidOperationException("There was multiple calls to " + _calls.First().GetMethodInfo().Name + " with an argument of type " + typeof(TArgument) + ". Use method call indexer."); + if (values.Count == 1) + return (TArgument) values[0]; + + throw new InvalidOperationException("None of the calls to '" + _calls.First().GetMethodInfo().Name + "' had an argument of type " + typeof(TArgument)); + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/ObjectExtensions.cs b/src/Server/Coderr.Server.App.Tests/ObjectExtensions.cs similarity index 88% rename from src/Server/OneTrueError.App.Tests/ObjectExtensions.cs rename to src/Server/Coderr.Server.App.Tests/ObjectExtensions.cs index 6b607398..864320cd 100644 --- a/src/Server/OneTrueError.App.Tests/ObjectExtensions.cs +++ b/src/Server/Coderr.Server.App.Tests/ObjectExtensions.cs @@ -1,15 +1,15 @@ -namespace OneTrueError.App.Tests -{ - internal static class ObjectExtensions - { - public static void SetId(this object instance, object id) - { - instance.GetType().GetProperty("Id").SetValue(instance, id); - } - - public static void SetProperty(this object instance, string name, object value) - { - instance.GetType().GetProperty(name).SetValue(instance, value); - } - } +namespace Coderr.Server.App.Tests +{ + internal static class ObjectExtensions + { + public static void SetId(this object instance, object id) + { + instance.GetType().GetProperty("Id").SetValue(instance, id); + } + + public static void SetProperty(this object instance, string name, object value) + { + instance.GetType().GetProperty(name).SetValue(instance, value); + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/PrincipalHelper.cs b/src/Server/Coderr.Server.App.Tests/PrincipalHelper.cs new file mode 100644 index 00000000..efd8bee6 --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/PrincipalHelper.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Security.Claims; +using Coderr.Server.Infrastructure.Security; + +namespace Coderr.Server.App.Tests +{ + class PrincipalHelper + { + public static ClaimsPrincipal Create(int userId, string userName) + { + var claims = new List() + { + new Claim(ClaimTypes.Name, userName), + new Claim(ClaimTypes.NameIdentifier, userId.ToString(), ClaimValueTypes.Integer32), + }; + var identity = new ClaimsIdentity(claims, AuthenticationTypes.Default); + return new ClaimsPrincipal(identity); + } + } +} diff --git a/src/Server/OneTrueError.App.Tests/ReposExtensions.cs b/src/Server/Coderr.Server.App.Tests/ReposExtensions.cs similarity index 85% rename from src/Server/OneTrueError.App.Tests/ReposExtensions.cs rename to src/Server/Coderr.Server.App.Tests/ReposExtensions.cs index 8f47963a..17bdc71e 100644 --- a/src/Server/OneTrueError.App.Tests/ReposExtensions.cs +++ b/src/Server/Coderr.Server.App.Tests/ReposExtensions.cs @@ -1,12 +1,12 @@ -using System; - -namespace OneTrueError.App.Tests -{ - internal static class ReposExtensions - { - public static void AssignId(this T instance, Action action) - { - //repos.When(x => x.CreateAsync(Arg.Any())).Do(x => x.Arg().SetId(3)); - } - } +using System; + +namespace Coderr.Server.App.Tests +{ + internal static class ReposExtensions + { + public static void AssignId(this T instance, Action action) + { + //repos.When(x => x.CreateAsync(Arg.Any())).Do(x => x.Arg().SetId(3)); + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/TestStore.cs b/src/Server/Coderr.Server.App.Tests/TestStore.cs new file mode 100644 index 00000000..b800b34f --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/TestStore.cs @@ -0,0 +1,21 @@ +using System; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Infrastructure.Configuration; + +namespace Coderr.Server.App.Tests +{ + public class TestStore : ConfigurationStore + { + public override T Load() + { + if (typeof(T) == typeof(BaseConfiguration)) + return (T) (object) new BaseConfiguration {BaseUrl = new Uri("http://localhost/")}; + + return new T(); + } + + public override void Store(IConfigurationSection section) + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App.Tests/Web/Overview/Queries/GetOverviewApplicationResultTests.cs b/src/Server/Coderr.Server.App.Tests/Web/Overview/Queries/GetOverviewApplicationResultTests.cs new file mode 100644 index 00000000..38d1e12f --- /dev/null +++ b/src/Server/Coderr.Server.App.Tests/Web/Overview/Queries/GetOverviewApplicationResultTests.cs @@ -0,0 +1,30 @@ +using System; +using Coderr.Server.Api.Web.Overview.Queries; +using FluentAssertions; +using Xunit; + +namespace Coderr.Server.App.Tests.Web.Overview.Queries +{ + public class GetOverviewApplicationResultTests + { + + [Fact] + public void ignore_future_dates_to_allow_malconfigured_clients() + { + + var sut = new GetOverviewApplicationResult("Label", DateTime.Today.AddDays(-30), 30); + sut.AddValue(DateTime.Today.AddDays(1), 10); + + } + + [Fact] + public void Should_allow_dates_within_the_given_interval() + { + + var sut = new GetOverviewApplicationResult("hello", DateTime.Today.AddDays(-30), 31); + sut.AddValue(DateTime.Today, 10); + + AssertionExtensions.Should((int) sut.Values[sut.Values.Length - 1]).Be(10); + } + } +} diff --git a/src/Server/Coderr.Server.App/AppType.cs b/src/Server/Coderr.Server.App/AppType.cs new file mode 100644 index 00000000..991d5a5a --- /dev/null +++ b/src/Server/Coderr.Server.App/AppType.cs @@ -0,0 +1,6 @@ +namespace Coderr.Server.App +{ + public class AppType + { + } +} diff --git a/src/Server/Coderr.Server.App/Coderr.Server.App.csproj b/src/Server/Coderr.Server.App/Coderr.Server.App.csproj new file mode 100644 index 00000000..cc6d2161 --- /dev/null +++ b/src/Server/Coderr.Server.App/Coderr.Server.App.csproj @@ -0,0 +1,41 @@ + + + netstandard2.0 + Coderr.Server.App + Coderr.Server.App + $(DefaultItemExcludes);*.DotSettings;*.vsspell;CustomDictionary.xml + Debug;Release;Premise + + + + + + + + + + + + + + + + NU1701 + + + + + + + + + + + + + + + + + + diff --git a/src/Server/Coderr.Server.App/Core/Accounts/AccountService.cs b/src/Server/Coderr.Server.App/Core/Accounts/AccountService.cs new file mode 100644 index 00000000..e6614f41 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Accounts/AccountService.cs @@ -0,0 +1,268 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Authentication; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Accounts.Events; +using Coderr.Server.Api.Core.Accounts.Requests; +using Coderr.Server.App.Core.Invitations; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Domain.Core.User; +using Coderr.Server.Infrastructure.Security; + +using DotNetCqs; +using log4net; + +namespace Coderr.Server.App.Core.Accounts +{ + /// + /// This is a service and not CQS object as the methods here are of RPC type which isn't really a good fit for commands + /// or queries. + /// + [ContainerService] + public class AccountService : IAccountService + { + private readonly ILog _logger = LogManager.GetLogger(typeof(AccountService)); + private readonly IAccountRepository _repository; + private readonly IMessageBus _messageBus; + private readonly IApplicationRepository _applicationRepository; + private IInvitationRepository _invitationRepository; + + public AccountService(IAccountRepository repository, IMessageBus messageBus, IApplicationRepository applicationRepository, IInvitationRepository invitationRepository) + { + _repository = repository; + _messageBus = messageBus; + _applicationRepository = applicationRepository; + _invitationRepository = invitationRepository; + } + + + public async Task ValidateLogin(string emailAddress, string userName) + { + var reply = new ValidateNewLoginReply(); + if (!string.IsNullOrEmpty(emailAddress)) + reply.EmailIsTaken = await _repository.IsEmailAddressTakenAsync(emailAddress); + + if (!string.IsNullOrEmpty(userName)) + reply.UserNameIsTaken = await _repository.FindByUserNameAsync(userName) != null; + + return reply; + } + + /// + /// Execute the request and generate a reply. + /// + /// Request to execute + /// + /// false if key was not found. + /// + public async Task ResetPassword(string activationKey, string newPassword) + { + var account = await _repository.FindByActivationKeyAsync(activationKey); + if (account == null) + return false; + + account.ChangePassword(newPassword); + await _repository.UpdateAsync(account); + return true; + } + + + /// + /// Execute the request and generate a reply. + /// + /// + /// + /// + /// Task which will contain the reply once completed. + /// + public async Task ActivateAccount(ClaimsPrincipal messagingPrincipal, string activationKey) + { + if (messagingPrincipal == null) throw new ArgumentNullException(nameof(messagingPrincipal)); + if (activationKey == null) throw new ArgumentNullException(nameof(activationKey)); + + var account = await _repository.FindByActivationKeyAsync(activationKey); + if (account == null) + throw new ArgumentOutOfRangeException(nameof(activationKey), activationKey, + "Key was not found."); + + account.Activate(); + await _repository.UpdateAsync(account); + + + if (!messagingPrincipal.IsCurrentAccount(account.Id)) + { + var evt = new AccountActivated(account.Id, account.UserName) + { + EmailAddress = account.Email + }; + await _messageBus.SendAsync(messagingPrincipal, evt); + } + + + var identity = await CreateIdentity(account.Id, account.UserName, account.IsSysAdmin); + return identity; + } + + /// + /// Execute the request and generate a reply. + /// + /// Request to execute + /// + /// Task which will contain the reply once completed. + /// + public async Task ChangePassword(int userId, string currentPassword, string newPassword) + { + var user = await _repository.GetByIdAsync(userId); + if (!user.ValidatePassword(currentPassword)) + return false; + + user.ChangePassword(newPassword); + await _repository.UpdateAsync(user); + return true; + } + + + /// + /// Execute the request and generate a reply. + /// + /// + /// + /// + /// Task which will contain the reply once completed. + /// + public async Task Login(string userName, string password) + { + if (userName == null) throw new ArgumentNullException(nameof(userName)); + if (password == null) throw new ArgumentNullException(nameof(password)); + + var account = await _repository.FindByUserNameAsync(userName); + + try + { + if (account == null || !account.Login(password)) + { + _logger.Debug("Logging in " + userName); + await _messageBus.SendAsync(new Message(new LoginFailed(userName) {InvalidLogin = true})); + if (account != null) + await _repository.UpdateAsync(account); + return null; + } + } + catch (AuthenticationException ex) + { + _logger.Debug("Logging failed for " + userName, ex); + _messageBus.SendAsync(new Message(new LoginFailed(userName) {IsLocked = true})).Wait(); + throw; + } + + await _repository.UpdateAsync(account); + var identity= await CreateIdentity(account.Id, account.UserName, account.IsSysAdmin); + return identity; + } + + public async Task CreateAsync(string userName, string email) + { + var password = Guid.NewGuid().ToString("N").Substring(0, 10); + var account = new Account(userName, password); + account.Activate(); + account.SetVerifiedEmail(email); + await _repository.CreateAsync(account); + await _messageBus.SendAsync(new AccountRegistered(account.Id, userName)); + return account; + } + + /// + /// Accepts and deletes the invitation. Sends an event which is picked up by the application domain (which transforms + /// the pending invite to a membership) + /// + /// Principal that outbound messages should be sent with + /// accept invitation DTO + /// + /// + /// Do note that an invitation can be accepted by using another email address than the one that the invitation was + /// sent to. So take care + /// when handling the event. Update the email that as used when sending the + /// invitation. + /// + /// + public async Task AcceptInvitation(ClaimsPrincipal messagingPrincipal, AcceptInvitation request) + { + var invitation = await _invitationRepository.GetByInvitationKeyAsync(request.InvitationKey); + if (invitation == null) + { + _logger.Error("Failed to find invitation key" + request.InvitationKey); + return null; + } + await _invitationRepository.DeleteAsync(request.InvitationKey); + + Account account; + if (request.AccountId == 0) + { + account = new Account(request.UserName, request.Password); + account.SetVerifiedEmail(request.AcceptedEmail); + account.Activate(); + account.Login(request.Password); + await _repository.CreateAsync(account); + } + else + { + account = await _repository.GetByIdAsync(request.AccountId); + account.SetVerifiedEmail(request.AcceptedEmail); + } + + var inviter = await _repository.FindByUserNameAsync(invitation.InvitedBy); + + ClaimsIdentity identity = null; + identity = await CreateIdentity(account.Id, account.UserName, account.IsSysAdmin); + + // Account have not been created before the invitation was accepted. + if (request.AccountId == 0) + { + await _messageBus.SendAsync(messagingPrincipal, new AccountRegistered(account.Id, account.UserName)); + await _messageBus.SendAsync(messagingPrincipal, new AccountActivated(account.Id, account.UserName) + { + EmailAddress = account.Email + }); + } + + var e = new InvitationAccepted(account.Id, invitation.InvitedBy, account.UserName) + { + InvitedEmailAddress = invitation.EmailToInvitedUser, + AcceptedEmailAddress = request.AcceptedEmail, + ApplicationIds = invitation.Invitations.Select(x => x.ApplicationId).ToArray() + }; + await _messageBus.SendAsync(messagingPrincipal, e); + + return identity; + } + + private async Task CreateIdentity(int accountId, string userName, bool isSysAdmin) + { + var claims = new List + { + new Claim(ClaimTypes.NameIdentifier, accountId.ToString(), ClaimValueTypes.Integer32), + new Claim(ClaimTypes.Name, userName, ClaimValueTypes.String) + }; + + var apps = await _applicationRepository.GetForUserAsync(accountId); + foreach (var app in apps) + { + claims.Add(new Claim(CoderrClaims.Application, app.ApplicationId.ToString(), ClaimValueTypes.Integer32)); + if (app.IsAdmin) + claims.Add(new Claim(CoderrClaims.ApplicationAdmin, app.ApplicationId.ToString(), ClaimValueTypes.Integer32)); + } + + + //accountId == 1 for backwards compatibility (with version 1.0) + if (isSysAdmin || accountId == 1) + claims.Add(new Claim(ClaimTypes.Role, CoderrRoles.SysAdmin)); + + return new ClaimsIdentity(claims.ToArray(), AuthenticationTypes.Default); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs new file mode 100644 index 00000000..caa2bd6d --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs @@ -0,0 +1,12 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.App.Core.Accounts.CommandHandlers.Entities +{ + /// + /// "private" entities which should not be exposed to the rest of the system. + /// + [CompilerGenerated] + internal class NamespaceDoc + { + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs new file mode 100644 index 00000000..b0b9ba3e --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs @@ -0,0 +1,144 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Api.Core.Accounts.Commands; +using Coderr.Server.Api.Core.Accounts.Events; +using Coderr.Server.Api.Core.Messaging; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.Infrastructure.Configuration; +using DotNetCqs; +using log4net; +using log4net.Appender; + +namespace Coderr.Server.App.Core.Accounts.CommandHandlers +{ + /// + /// Register a new account. + /// + /// + /// + /// Will wait for activation before allowing the user to login. + /// + /// + public class RegisterAccountHandler : IMessageHandler + { + private readonly IAccountRepository _repository; + private ILog _logger = LogManager.GetLogger(typeof(RegisterAccountHandler)); + private ConfigurationStore _configStore; + + /// + /// Creates a new instance of . + /// + /// repos + public RegisterAccountHandler(IAccountRepository repository, ConfigurationStore configStore) + { + _repository = repository; + _configStore = configStore; + } + + /// + /// Execute a command asynchronously. + /// + /// Command to execute. + /// + /// Task which will be completed once the command has been executed. + /// + public async Task HandleAsync(IMessageContext context, RegisterAccount command) + { + if (command == null) throw new ArgumentNullException("command"); + + if (!string.IsNullOrEmpty(command.UserName) && await _repository.IsUserNameTakenAsync(command.UserName)) + { + _logger.Warn("UserName is taken: " + command.UserName); + await SendAccountInfo(context, command.UserName); + return; + } + + var account = command.AccountId > 0 + ? new Account(command.AccountId, command.UserName, command.Password) + : new Account(command.UserName, command.Password); + account.SetVerifiedEmail(command.Email); + + if (command.ActivateDirectly) + { + _logger.Debug("Activating directly"); + account.Activate(); + } + + var accountCount = await _repository.CountAsync(); + if (accountCount == 0) + account.IsSysAdmin = true; + + await _repository.CreateAsync(account); + + // accounts can be activated directly. + // should not send activation email then. + if (account.AccountState == AccountState.VerificationRequired) + await SendVerificationEmail(context, account, command.ReturnUrl); + + var evt = new AccountRegistered(account.Id, account.UserName) { IsSysAdmin = account.IsSysAdmin }; + await context.SendAsync(evt); + + if (command.ActivateDirectly) + { + var evt1 = new AccountActivated(account.Id, account.UserName) { EmailAddress = account.Email }; + await context.SendAsync(evt1); + } + } + + private async Task SendAccountInfo(IMessageContext context, string userName) + { + var account = await _repository.GetByUserNameAsync(userName); + + var config = _configStore.Load(); + var email = config.SupportEmail; + //TODO: HTML email + var msg = new EmailMessage + { + TextBody = $@"Hello! + +Someone (you?) tried to create an account with the same login as your account. + +If it was you, you can request a new password from the login page. Otherwise, +contact us so that we can investigate: {email} + +Regards, + Support team", + Subject = "Coderr registration" + }; + msg.Recipients = new[] { new EmailAddress(account.Email) }; + + await context.SendAsync(new SendEmail(msg)); + } + + private Task SendVerificationEmail(IMessageContext context, Account account, string returnUrl) + { + var config = _configStore.Load(); + + var url = $"{config.BaseUrl.ToString().TrimEnd('/')}/account/activate/{account.ActivationKey}"; + if (returnUrl != null) + { + url += "?returnUrl=" + returnUrl; + } + + //TODO: HTML email + var msg = new EmailMessage + { + TextBody = string.Format(@"Welcome, + + +Your activation code is: {0} + +You can activate your account by clicking on: {1} + +Good luck, + Coderr Team", account.ActivationKey, url), + Subject = "Coderr activation" + }; + msg.Recipients = new[] { new EmailAddress(account.Email) }; + + return context.SendAsync(new SendEmail(msg)); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs new file mode 100644 index 00000000..d0269576 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs @@ -0,0 +1,119 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Api.Core.Accounts; +using Coderr.Server.Api.Core.Accounts.Commands; +using Coderr.Server.Api.Core.Accounts.Events; +using Coderr.Server.Api.Core.Messaging; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.Infrastructure.Configuration; +using DotNetCqs; + +using log4net; + +namespace Coderr.Server.App.Core.Accounts.CommandHandlers +{ + /// + /// Handler for . + /// + internal class RegisterSimpleHandler : IMessageHandler + { + private readonly ILog _logger = LogManager.GetLogger(typeof(RegisterSimpleHandler)); + private readonly IAccountRepository _repository; + private readonly ConfigurationStore _configStore; + + public RegisterSimpleHandler(IAccountRepository repository, ConfigurationStore configStore) + { + _repository = repository; + _configStore = configStore; + } + + public async Task HandleAsync(IMessageContext context, RegisterSimple command) + { + var pos = command.EmailAddress.IndexOf('@'); + if (pos == -1) + { + _logger.Warn("Invalid email address: " + command.EmailAddress); + throw new InvalidOperationException("Invalid email address"); + } + + var user = _repository.FindByEmailAsync(command.EmailAddress); + if (user != null) + { + _logger.Warn("Email already taken, sending reset password: " + command.EmailAddress); + await context.SendAsync(new RequestPasswordReset(command.EmailAddress)); + } + + var userName = await TryCreateUsernameAsync(command, pos); + if (userName == null) + { + _logger.Error("Failed to generate user name for " + command.EmailAddress); + return; + } + + + //var id = _idGeneratorClient.GetNextId(Account.SEQUENCE); + var password = Guid.NewGuid().ToString("N").Substring(0, 10); + var account = new Account(userName, password); + account.SetVerifiedEmail(command.EmailAddress); + await _repository.CreateAsync(account); + + await SendAccountEmail(context, account, password); + + var evt = new AccountRegistered(account.Id, account.UserName); + await context.SendAsync(evt); + } + + private Task SendAccountEmail(IMessageContext context, Account account, string password) + { + var config = _configStore.Load(); + //TODO: HTML email + var msg = new EmailMessage + { + TextBody = string.Format(@"Welcome, + + +We have created your account. + +UserName: {1} +Password: {2} + +You can login using {0}/account/activate/{3}. + +We recommend that you change your password before doing something useful. + +Thanks, + The Coderr Team", config.BaseUrl, account.UserName, password, account.ActivationKey), + Subject = "Coderr activation" + }; + msg.Recipients = new[] {new EmailAddress(account.Email)}; + + return context.SendAsync(new SendEmail(msg)); + } + + private async Task TryCreateUsernameAsync(RegisterSimple command, int pos) + { + var suggestedUserName = command.EmailAddress.Substring(0, pos); + if (!await _repository.IsUserNameTakenAsync(suggestedUserName)) + return suggestedUserName; + + var counter = 100; + var newUserName = suggestedUserName + counter; + while (counter < 110) + { + if (!await _repository.IsUserNameTakenAsync(newUserName)) + { + suggestedUserName = newUserName; + return suggestedUserName; + } + + counter++; + newUserName = suggestedUserName + counter; + } + + _logger.Error("Failed to generate userName: " + suggestedUserName); + return null; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs new file mode 100644 index 00000000..04d48c7d --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs @@ -0,0 +1,57 @@ +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Api.Core.Accounts.Commands; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.Infrastructure.Configuration; +using DotNetCqs; + +using log4net; + +namespace Coderr.Server.App.Core.Accounts.CommandHandlers +{ + /// + /// Handler for . + /// + internal class RequestPasswordResetHandler : IMessageHandler + { + private readonly IAccountRepository _accountRepository; + private readonly BaseConfiguration _baseConfig; + private readonly ILog _logger = LogManager.GetLogger(typeof(RequestPasswordResetHandler)); + + public RequestPasswordResetHandler(IAccountRepository accountRepository, IConfiguration baseConfig) + { + _accountRepository = accountRepository; + _baseConfig = baseConfig.Value; + } + + public async Task HandleAsync(IMessageContext context, RequestPasswordReset command) + { + var account = await _accountRepository.FindByEmailAsync(command.EmailAddress); + if (account == null) + { + _logger.Warn("Failed to find a user with email " + command.EmailAddress); + return; + } + + account.RequestPasswordReset(); + await _accountRepository.UpdateAsync(account); + + var cmd = new SendTemplateEmail("Password reset", "ResetPassword") + { + To = account.Email, + Model = + new + { + AccountName = account.UserName, + ResetLink = + _baseConfig.BaseUrl + "password/reset/" + + account.ActivationKey + }, + Subject = "Reset password" + }; + + await context.SendAsync(cmd); + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/Templates/ResetPassword/Template.md b/src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/Templates/ResetPassword/Template.md similarity index 100% rename from src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/Templates/ResetPassword/Template.md rename to src/Server/Coderr.Server.App/Core/Accounts/CommandHandlers/Templates/ResetPassword/Template.md diff --git a/src/Server/Coderr.Server.App/Core/Accounts/IAccountService.cs b/src/Server/Coderr.Server.App/Core/Accounts/IAccountService.cs new file mode 100644 index 00000000..f5b3ecc5 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Accounts/IAccountService.cs @@ -0,0 +1,81 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Events; +using Coderr.Server.Api.Core.Accounts.Requests; +using Coderr.Server.Domain.Core.Account; + +namespace Coderr.Server.App.Core.Accounts +{ + public interface IAccountService + { + /// + /// Accepts and deletes the invitation. Sends an event which is picked up by the application domain (which transforms + /// the pending invite to a membership) + /// + /// Logged in user. + /// Information about the accept invitation request + /// Updated identity with permission to the applications that the user was invited to. + /// + /// + /// Do note that an invitation can be accepted by using another email address than the one that the invitation was + /// sent to. So take care + /// when handling the event. Update the email that as used when sending the + /// invitation. + /// + /// + Task AcceptInvitation(ClaimsPrincipal messagingPrincipal, AcceptInvitation request); + + /// + /// Validate login information (check if the specified information is available). + /// + /// Email address as specified by the user. + /// Wanted userName + /// + Task ValidateLogin(string emailAddress, string userName); + + /// + /// Execute the request and generate a reply. + /// + /// Key from sent email + /// password + /// + /// Task which will contain the reply once completed. + /// + Task ResetPassword(string activationKey, string newPassword); + + /// + /// Execute the request and generate a reply. + /// + /// Currently logged in user (if logged in) + /// Activation key + /// + /// Task which will contain the reply once completed. + /// + Task ActivateAccount(ClaimsPrincipal user, string activationKey); + + /// + /// Execute the request and generate a reply. + /// + /// Request to execute + /// + /// Task which will contain the reply once completed. + /// + Task ChangePassword(int userId, string currentPassword, string newPassword); + + /// + /// Execute the request and generate a reply. + /// + /// Entered user name + /// clear text password + /// + /// Task which will contain the reply once completed. + /// + Task Login(string userName, string password); + + /// + /// Create a new account for the given userName + /// + /// + Task CreateAsync(string userName, string email); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Accounts/NamespaceDoc.cs b/src/Server/Coderr.Server.App/Core/Accounts/NamespaceDoc.cs new file mode 100644 index 00000000..3b29b851 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Accounts/NamespaceDoc.cs @@ -0,0 +1,18 @@ +using System.Runtime.CompilerServices; + +namespace Coderr.Server.App.Core.Accounts +{ + // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. + + /// + /// Accounts + /// + /// + /// Accounts are used to handle authentication and site wide authorization. + /// + /// + [CompilerGenerated] + class NamespaceDoc + { + } +} diff --git a/src/Server/OneTrueError.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs b/src/Server/Coderr.Server.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs similarity index 82% rename from src/Server/OneTrueError.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs rename to src/Server/Coderr.Server.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs index 906b472f..f9693e60 100644 --- a/src/Server/OneTrueError.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs +++ b/src/Server/Coderr.Server.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs @@ -1,42 +1,42 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Queries; - -namespace OneTrueError.App.Core.Accounts.Queries -{ - /// - /// Handler of . - /// - [Component] - public class FindAccountByUserNameHandler : IQueryHandler - { - private readonly IAccountRepository _repository; - - /// - /// Creates a new instance of . - /// - /// - public FindAccountByUserNameHandler(IAccountRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - /// Method used to execute the query - /// - /// Query to execute. - /// - /// Task which will contain the result once completed. - /// - public async Task ExecuteAsync(FindAccountByUserName query) - { - if (query == null) throw new ArgumentNullException("query"); - - var user = await _repository.FindByUserNameAsync(query.UserName); - return user == null ? null : new FindAccountByUserNameResult(user.Id, user.UserName); - } - } +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Queries; +using Coderr.Server.Domain.Core.Account; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Accounts.Queries +{ + /// + /// Handler of . + /// + public class FindAccountByUserNameHandler : IQueryHandler + { + private readonly IAccountRepository _repository; + + /// + /// Creates a new instance of . + /// + /// + public FindAccountByUserNameHandler(IAccountRepository repository) + { + if (repository == null) throw new ArgumentNullException("repository"); + _repository = repository; + } + + /// + /// Method used to execute the query + /// + /// Query to execute. + /// + /// Task which will contain the result once completed. + /// + public async Task HandleAsync(IMessageContext context, FindAccountByUserName query) + { + if (query == null) throw new ArgumentNullException("query"); + + var user = await _repository.FindByUserNameAsync(query.UserName); + return user == null ? null : new FindAccountByUserNameResult(user.Id, user.UserName); + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Queries/GetAccountQueryHandler.cs b/src/Server/Coderr.Server.App/Core/Accounts/Queries/GetAccountQueryHandler.cs similarity index 81% rename from src/Server/OneTrueError.App/Core/Accounts/Queries/GetAccountQueryHandler.cs rename to src/Server/Coderr.Server.App/Core/Accounts/Queries/GetAccountQueryHandler.cs index c10df8a1..f598f005 100644 --- a/src/Server/OneTrueError.App/Core/Accounts/Queries/GetAccountQueryHandler.cs +++ b/src/Server/Coderr.Server.App/Core/Accounts/Queries/GetAccountQueryHandler.cs @@ -1,58 +1,60 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Queries; - -namespace OneTrueError.App.Core.Accounts.Queries -{ - /// - /// Handler for - /// - [Component] - public class GetAccountQueryHandler : IQueryHandler - { - private readonly IAccountRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - public GetAccountQueryHandler(IAccountRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - /// Method used to execute the query - /// - /// Query to execute. - /// - /// Task which will contain the result once completed. - /// - public async Task ExecuteAsync(GetAccountById query) - { - if (query == null) throw new ArgumentNullException("query"); - - var account = await _repository.GetByIdAsync(query.AccountId); - if (account == null) - return null; - - var dto = new AccountDTO - { - CreatedAtUtc = account.CreatedAtUtc, - Email = account.Email, - Id = account.Id, - LastLoginAtUtc = account.LastLoginAtUtc, - State = - (Api.Core.Accounts.Queries.AccountState) - Enum.Parse(typeof(Api.Core.Accounts.Queries.AccountState), account.AccountState.ToString()), - UpdatedAtUtc = account.UpdatedAtUtc, - UserName = account.UserName - }; - - return dto; - } - } +using System; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Queries; +using Coderr.Server.Domain.Core.Account; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Accounts.Queries +{ + /// + /// Handler for + /// + public class GetAccountQueryHandler : IQueryHandler + { + private readonly IAccountRepository _repository; + + /// + /// Creates a new instance of . + /// + /// repos + public GetAccountQueryHandler(IAccountRepository repository) + { + if (repository == null) throw new ArgumentNullException("repository"); + _repository = repository; + } + + /// + /// Method used to execute the query + /// + /// Query to execute. + /// + /// Task which will contain the result once completed. + /// + public async Task HandleAsync(IMessageContext context, GetAccountById query) + { + if (query == null) throw new ArgumentNullException("query"); + + var accounts = await _repository.GetByIdAsync(new[] { query.AccountId }); + var account = accounts?.FirstOrDefault(); + if (account == null) + return null; + + var dto = new AccountDTO + { + CreatedAtUtc = account.CreatedAtUtc, + Email = account.Email, + Id = account.Id, + LastLoginAtUtc = account.LastLoginAtUtc, + State = + (Api.Core.Accounts.Queries.AccountState) + Enum.Parse(typeof(Api.Core.Accounts.Queries.AccountState), account.AccountState.ToString()), + UpdatedAtUtc = account.UpdatedAtUtc, + UserName = account.UserName + }; + + return dto; + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/ReadMe.md b/src/Server/Coderr.Server.App/Core/Accounts/ReadMe.md similarity index 100% rename from src/Server/OneTrueError.App/Core/Accounts/ReadMe.md rename to src/Server/Coderr.Server.App/Core/Accounts/ReadMe.md diff --git a/src/Server/Coderr.Server.App/Core/ApiKeys/ApiKey.cs b/src/Server/Coderr.Server.App/Core/ApiKeys/ApiKey.cs new file mode 100644 index 00000000..151bd21c --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/ApiKeys/ApiKey.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Coderr.Server.Abstractions.Security; + +namespace Coderr.Server.App.Core.ApiKeys +{ + /// + /// A generated API key which can be used to call Coderr´s HTTP api. + /// + public class ApiKey + { + private List _claims = new List(); + + /// + /// Application that will be using this key + /// + public string ApplicationName { get; set; } + + + /// + /// Claims associated with this key + /// + /// + /// + /// Typically contains to identity which applications the key can access. + /// + /// If no applications are specified, then the key have access to all apps + /// + public Claim[] Claims + { + get => _claims.Any() + ? _claims.ToArray() + : new[] { new Claim(ClaimTypes.Role, CoderrRoles.SysAdmin) }; + private set => _claims = new List(value); + } + /// + /// When this key was generated + /// + public DateTime CreatedAtUtc { get; set; } + + /// + /// AccountId that generated this key + /// + public int CreatedById { get; set; } + + /// + /// Api key + /// + public string GeneratedKey { get; set; } + + /// + /// PK + /// + public int Id { get; set; } + + + /// + /// Used when generating signatures. + /// + public string SharedSecret { get; set; } + + /// + /// Add an application that this ApiKey can be used for. + /// + /// application id + public void Add(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + + _claims.Add(new Claim(CoderrClaims.Application, applicationId.ToString(), ClaimValueTypes.Integer32)); + + // Api clients typically are allowed to manage everything. Let's do that! + _claims.Add(new Claim(CoderrClaims.ApplicationAdmin, applicationId.ToString(), ClaimValueTypes.Integer32)); + } + + /// + /// Validate a given signature using the HTTP body. + /// + /// Signature passed from the client + /// HTTP body (i.e. the data that the signature was generated on) + /// true if the signature was generated using the shared secret; otherwise false. + public bool ValidateSignature(string specifiedSignature, byte[] body) + { + var hashAlgo = new HMACSHA256(Encoding.UTF8.GetBytes(SharedSecret.ToLower())); + var hash = hashAlgo.ComputeHash(body); + var signature = Convert.ToBase64String(hash); + + var hashAlgo1 = new HMACSHA256(Encoding.UTF8.GetBytes(SharedSecret.ToUpper())); + var hash1 = hashAlgo1.ComputeHash(body); + var signature1 = Convert.ToBase64String(hash1); + + return specifiedSignature.Equals(signature) || specifiedSignature.Equals(signature1); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/ApiKeys/Events/ApplicationDeletedHandler.cs b/src/Server/Coderr.Server.App/Core/ApiKeys/Events/ApplicationDeletedHandler.cs new file mode 100644 index 00000000..984dcd0e --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/ApiKeys/Events/ApplicationDeletedHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Events; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.ApiKeys.Events +{ + /// + /// Will either delete an entire apikey (if the only association is with the given application) or just remove the + /// application mapping. + /// + public class ApplicationDeletedHandler : IMessageHandler + { + private readonly IApiKeyRepository _repository; + + /// + /// Creates a new instance of . + /// + /// repos + public ApplicationDeletedHandler(IApiKeyRepository repository) + { + if (repository == null) throw new ArgumentNullException("repository"); + _repository = repository; + } + + /// + public async Task HandleAsync(IMessageContext context, ApplicationDeleted e) + { + var apps = await _repository.GetForApplicationAsync(e.ApplicationId); + foreach (var apiKey in apps) + { + if (apiKey.Claims.Length == 1) + await _repository.DeleteAsync(apiKey.Id); + else + await _repository.DeleteApplicationMappingAsync(apiKey.Id, e.ApplicationId); + } + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/ApiKeys/IApiKeyRepository.cs b/src/Server/Coderr.Server.App/Core/ApiKeys/IApiKeyRepository.cs similarity index 94% rename from src/Server/OneTrueError.App/Core/ApiKeys/IApiKeyRepository.cs rename to src/Server/Coderr.Server.App/Core/ApiKeys/IApiKeyRepository.cs index b2d9cbb9..44d044cd 100644 --- a/src/Server/OneTrueError.App/Core/ApiKeys/IApiKeyRepository.cs +++ b/src/Server/Coderr.Server.App/Core/ApiKeys/IApiKeyRepository.cs @@ -1,42 +1,42 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Griffin.Data; - -namespace OneTrueError.App.Core.ApiKeys -{ - /// - /// Repository for . - /// - public interface IApiKeyRepository - { - /// - /// Delete all mappings that are for a specific application - /// - /// id for the ApiKey that the application is associated with - /// Application to remove mapping for - /// - Task DeleteApplicationMappingAsync(int apiKeyId, int applicationId); - - /// - /// Delete a specific ApiKey. - /// - /// - /// - Task DeleteAsync(int keyId); - - /// - /// Get an key by using the generated string. - /// - /// key - /// key - /// Given key was not found. - Task GetByKeyAsync(string apiKey); - - /// - /// Get all ApiKeys that maps to a specific application - /// - /// application id - /// list - Task> GetForApplicationAsync(int applicationId); - } +using System.Collections.Generic; +using System.Threading.Tasks; +using Griffin.Data; + +namespace Coderr.Server.App.Core.ApiKeys +{ + /// + /// Repository for . + /// + public interface IApiKeyRepository + { + /// + /// Delete all mappings that are for a specific application + /// + /// id for the ApiKey that the application is associated with + /// Application to remove mapping for + /// + Task DeleteApplicationMappingAsync(int apiKeyId, int applicationId); + + /// + /// Delete a specific ApiKey. + /// + /// + /// + Task DeleteAsync(int keyId); + + /// + /// Get an key by using the generated string. + /// + /// key + /// key + /// Given key was not found. + Task GetByKeyAsync(string apiKey); + + /// + /// Get all ApiKeys that maps to a specific application + /// + /// application id + /// list + Task> GetForApplicationAsync(int applicationId); + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/ApiKeys/ReadMe.md b/src/Server/Coderr.Server.App/Core/ApiKeys/ReadMe.md new file mode 100644 index 00000000..d5541dbe --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/ApiKeys/ReadMe.md @@ -0,0 +1,5 @@ +Api Keys +========= + +Api keys are used to allow other applications to communicate with codeRR through the HTTP api. + diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/AddTeamMemberHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/AddTeamMemberHandler.cs new file mode 100644 index 00000000..f31e44e3 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/AddTeamMemberHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Commands; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + public class AddTeamMemberHandler : IMessageHandler + { + private readonly IApplicationRepository _applicationRepository; + + public AddTeamMemberHandler(IApplicationRepository applicationRepository) + { + _applicationRepository = applicationRepository; + } + + public async Task HandleAsync(IMessageContext context, AddTeamMember message) + { + var member = new ApplicationTeamMember(message.ApplicationId, message.UserToAdd, + context.Principal.Identity.Name); + + await _applicationRepository.CreateAsync(member); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/CreateApplicationHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/CreateApplicationHandler.cs new file mode 100644 index 00000000..d932d63c --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/CreateApplicationHandler.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Commands; +using Coderr.Server.Api.Core.Applications.Events; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Domain.Core.User; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + internal class CreateApplicationHandler : IMessageHandler + { + private readonly IApplicationRepository _repository; + private readonly IUserRepository _userRepository; + + public CreateApplicationHandler(IApplicationRepository repository, IUserRepository userRepository) + { + _repository = repository; + _userRepository = userRepository; + } + + public async Task HandleAsync(IMessageContext context, CreateApplication command) + { + var app = new Application(command.UserId, command.Name) + { + ApplicationType = + (TypeOfApplication)Enum.Parse(typeof(TypeOfApplication), command.TypeOfApplication.ToString()), + }; + if (command.GroupId == null) + { + app.GroupId = await _repository.GetFirstGroupIdAsync(); + } + else + { + app.GroupId = command.GroupId.Value; + } + + if (command.ApplicationKey != null) + app.AppKey = command.ApplicationKey; + + + if (command.NumberOfDevelopers > 0) + { + app.AddStatsBase(command.NumberOfDevelopers, command.NumberOfErrors); + } + + app.RetentionDays = command.RetentionDays ?? 60; + + var creator = await _userRepository.GetUserAsync(command.UserId); + await _repository.CreateAsync(app); + await _repository.CreateAsync(new ApplicationTeamMember(app.Id, creator.AccountId, creator.UserName) + { + UserName = creator.UserName, + Roles = new[] { ApplicationRole.Admin, ApplicationRole.Member }, + }); + + var evt = new ApplicationCreated(app.Id, app.Name, command.UserId, app.AppKey, app.SharedSecret); + await context.SendAsync(evt); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/DeleteApplicationHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/DeleteApplicationHandler.cs new file mode 100644 index 00000000..8d57c8a0 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/DeleteApplicationHandler.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Applications.Commands; +using Coderr.Server.Api.Core.Applications.Events; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Infrastructure.Security; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + /// + /// Handler for . + /// + public class DeleteApplicationHandler : IMessageHandler + { + private readonly IApplicationRepository _repository; + + /// + /// Creates a new instance of . + /// + /// used to delete the application + public DeleteApplicationHandler(IApplicationRepository repository) + { + _repository = repository; + } + + /// + public async Task HandleAsync(IMessageContext context, DeleteApplication command) + { + context.Principal.EnsureApplicationAdmin(command.Id); + + var app = await _repository.GetByIdAsync(command.Id); + await _repository.DeleteAsync(command.Id); + var evt = new ApplicationDeleted {ApplicationName = app.Name, ApplicationId = app.Id, AppKey = app.AppKey}; + await context.SendAsync(evt); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/MapApplicationsToGroupHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/MapApplicationsToGroupHandler.cs new file mode 100644 index 00000000..8b9fd60b --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/MapApplicationsToGroupHandler.cs @@ -0,0 +1,40 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Commands; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + class MapApplicationsToGroupHandler : IMessageHandler + { + private readonly IAdoNetUnitOfWork _uow; + + public MapApplicationsToGroupHandler(IAdoNetUnitOfWork uow) + { + _uow = uow ?? throw new ArgumentNullException(nameof(uow)); + } + + public async Task HandleAsync(IMessageContext context, MapApplicationsToGroup message) + { + _uow.ExecuteNonQuery($"DELETE FROM ApplicationGroupMap WHERE ApplicationGroupId=@groupId", new { groupId = message.GroupId }); + + using (var cmd = _uow.CreateDbCommand()) + { + var sql = "INSERT INTO ApplicationGroupMap (ApplicationGroupId, ApplicationId) VALUES"; + foreach (var appId in message.ApplicationIds) + { + sql += $"({message.GroupId}, {appId}),\r\n"; + } + + //last ",crlf " + sql = sql.Remove(sql.Length - 3, 3); + cmd.CommandText = sql; + await cmd.ExecuteNonQueryAsync(); + } + + var ids = string.Join(", ", message.ApplicationIds); + _uow.ExecuteNonQuery($"DELETE FROM ApplicationGroupMap WHERE ApplicationGroupId = 1 AND ApplicationId IN ({ids})"); + } + } +} diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/MuteStatsHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/MuteStatsHandler.cs new file mode 100644 index 00000000..c1d41ef3 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/MuteStatsHandler.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Commands; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + class MuteStatisticsQuestionHandler : IMessageHandler + { + private readonly IApplicationRepository _applicationRepository; + + public MuteStatisticsQuestionHandler(IApplicationRepository applicationRepository) + { + _applicationRepository = applicationRepository ?? throw new ArgumentNullException(nameof(applicationRepository)); + } + + public async Task HandleAsync(IMessageContext context, MuteStatisticsQuestion message) + { + var app = await _applicationRepository.GetByIdAsync(message.ApplicationId); + app.MuteStatisticsQuestion = true; + await _applicationRepository.UpdateAsync(app); + } + } +} diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/RemoveTeamMemberHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/RemoveTeamMemberHandler.cs new file mode 100644 index 00000000..a2ce4e09 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/RemoveTeamMemberHandler.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Applications.Commands; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Infrastructure.Security; +using DotNetCqs; + +using log4net; + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + /// + /// Remove a team member from an application + /// + public class RemoveTeamMemberHandler : IMessageHandler + { + private readonly IApplicationRepository _applicationRepository; + private readonly ILog _logger = LogManager.GetLogger(typeof(RemoveTeamMember)); + + /// + /// Creates a new instance of . + /// + /// To remove member + public RemoveTeamMemberHandler(IApplicationRepository applicationRepository) + { + _applicationRepository = applicationRepository; + } + + /// + public async Task HandleAsync(IMessageContext context, RemoveTeamMember command) + { + context.Principal.EnsureApplicationAdmin(command.ApplicationId); + await _applicationRepository.RemoveTeamMemberAsync(command.ApplicationId, command.UserToRemove); + _logger.Info("Removed " + command.UserToRemove + " from application " + command.ApplicationId); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/RenameGroupHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/RenameGroupHandler.cs new file mode 100644 index 00000000..2cf7fdbd --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/RenameGroupHandler.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Commands; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + public class RenameGroupHandler : IMessageHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public RenameGroupHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork)); + } + + public Task HandleAsync(IMessageContext context, RenameApplicationGroup message) + { + _unitOfWork.Execute("UPDATE ApplicationGroups SET Name = @name WHERE Id = @id", + new {name = message.NewName, id = message.GroupId}); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/UpdateApplicationHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/UpdateApplicationHandler.cs new file mode 100644 index 00000000..bed63748 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/UpdateApplicationHandler.cs @@ -0,0 +1,41 @@ +using System.Threading.Tasks; +using Coderr.Server.Api; +using Coderr.Server.Api.Core.Applications.Commands; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + /// + /// Used to update application name and applicationType. + /// + public class UpdateApplicationHandler : IMessageHandler + { + private readonly IApplicationRepository _repository; + + /// + /// Creates a new instance of . + /// + /// repos + public UpdateApplicationHandler(IApplicationRepository repository) + { + _repository = repository; + } + + /// + public async Task HandleAsync(IMessageContext context, UpdateApplication command) + { + var app = await _repository.GetByIdAsync(command.ApplicationId); + app.Name = command.Name; + if (command.TypeOfApplication != null) + { + app.ApplicationType = command.TypeOfApplication.Value.ConvertEnum(); + } + + app.RetentionDays = command.RetentionDays ?? 60; + + await _repository.UpdateAsync(app); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/UpdateRolesHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/UpdateRolesHandler.cs new file mode 100644 index 00000000..eabcdb32 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/CommandHandlers/UpdateRolesHandler.cs @@ -0,0 +1,26 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Commands; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Applications.CommandHandlers +{ + public class UpdateRolesHandler : IMessageHandler + { + private readonly IApplicationRepository _applicationRepository; + + public UpdateRolesHandler(IApplicationRepository applicationRepository) + { + _applicationRepository = applicationRepository; + } + + public async Task HandleAsync(IMessageContext context, UpdateRoles message) + { + var apps = await _applicationRepository.GetTeamMembersAsync(message.ApplicationId); + var user = apps.FirstOrDefault(x => x.AccountId == message.UserToUpdate); + user.Roles = message.Roles; + await _applicationRepository.UpdateAsync(user); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/EventHandlers/UpdateTeamOnInvitationAccepted.cs b/src/Server/Coderr.Server.App/Core/Applications/EventHandlers/UpdateTeamOnInvitationAccepted.cs new file mode 100644 index 00000000..64d29e9d --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/EventHandlers/UpdateTeamOnInvitationAccepted.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Events; +using Coderr.Server.Api.Core.Applications.Events; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Applications.EventHandlers +{ + internal class UpdateTeamOnInvitationAccepted : IMessageHandler + { + private readonly IApplicationRepository _applicationRepository; + + public UpdateTeamOnInvitationAccepted(IApplicationRepository applicationRepository) + { + _applicationRepository = applicationRepository; + } + + public async Task HandleAsync(IMessageContext context, InvitationAccepted e) + { + foreach (var applicationId in e.ApplicationIds) + { + var members = await _applicationRepository.GetTeamMembersAsync(applicationId); + var member = members.FirstOrDefault(x => x.EmailAddress == e.InvitedEmailAddress && x.AccountId == 0); + if (member != null) + { + member.AcceptInvitation(e.AccountId); + await _applicationRepository.UpdateAsync(member); + await context.SendAsync(new UserAddedToApplication(applicationId, e.AccountId)); + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetApplicationInfoHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetApplicationInfoHandler.cs new file mode 100644 index 00000000..2fdcefd9 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetApplicationInfoHandler.cs @@ -0,0 +1,80 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api; +using Coderr.Server.Api.Core.Applications.Queries; +using Coderr.Server.App.Modules.Versions; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Modules.ApplicationVersions; +using DotNetCqs; +using TypeOfApplication = Coderr.Server.Api.Core.Applications.TypeOfApplication; + +namespace Coderr.Server.App.Core.Applications.QueryHandlers +{ + /// + /// Handler for . + /// + public class GetApplicationInfoHandler : IQueryHandler + { + private readonly IIncidentRepository _incidentRepository; + private readonly IApplicationRepository _repository; + private readonly IApplicationVersionRepository _versionRepository; + + /// + /// Creates a new instance of . + /// + /// repos + /// used to count the number of incidents + /// to fetch versions + public GetApplicationInfoHandler(IApplicationRepository repository, IIncidentRepository incidentRepository, IApplicationVersionRepository versionRepository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _incidentRepository = incidentRepository ?? throw new ArgumentNullException(nameof(incidentRepository)); + _versionRepository = versionRepository ?? throw new ArgumentNullException(nameof(versionRepository)); + } + + /// + /// Method used to execute the query + /// + /// Query to execute. + /// + /// Task which will contain the result once completed. + /// + public async Task HandleAsync(IMessageContext context, GetApplicationInfo query) + { + Application app; + if (!string.IsNullOrEmpty(query.AppKey)) + { + app = await _repository.GetByKeyAsync(query.AppKey); + } + else + { + app = await _repository.GetByIdAsync(query.ApplicationId); + } + + var newestErrorDate = await _incidentRepository.GetLatestIncidentDate(query.ApplicationId); + var totalCount = await _incidentRepository.GetTotalCountForAppInfoAsync(app.Id); + var versions = await _versionRepository.FindVersionsAsync(app.Id); + if (totalCount == 0) + { + versions = new string[0]; + } + + return new GetApplicationInfoResult + { + AppKey = app.AppKey, + ApplicationType = app.ApplicationType.ConvertEnum(), + Id = app.Id, + Name = app.Name, + SharedSecret = app.SharedSecret, + RetentionDays = app.RetentionDays, + LastIncidentAtUtc = newestErrorDate == DateTime.MinValue ? (DateTime?)null : newestErrorDate, + TotalIncidentCount = totalCount, + Versions = versions.ToArray(), + ShowStatsQuestion = !app.MuteStatisticsQuestion, + NumberOfDevelopers = app.NumberOfFtes + }; + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetApplicationTeamHandler.cs b/src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetApplicationTeamHandler.cs similarity index 76% rename from src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetApplicationTeamHandler.cs rename to src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetApplicationTeamHandler.cs index abf33c23..7e171b9a 100644 --- a/src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetApplicationTeamHandler.cs +++ b/src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetApplicationTeamHandler.cs @@ -1,41 +1,42 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Applications.Queries; - -namespace OneTrueError.App.Core.Applications.QueryHandlers -{ - [Component] - internal class GetApplicationTeamHandler : IQueryHandler - { - private readonly IApplicationRepository _applicationRepository; - - public GetApplicationTeamHandler(IApplicationRepository applicationRepository) - { - _applicationRepository = applicationRepository; - } - - public async Task ExecuteAsync(GetApplicationTeam query) - { - var members = await _applicationRepository.GetTeamMembersAsync(query.ApplicationId); - var result = new GetApplicationTeamResult - { - Invited = members.Where(x => x.AccountId == 0).Select(x => new GetApplicationTeamResultInvitation - { - EmailAddress = x.EmailAddress, - InvitedAtUtc = x.AddedAtUtc, - InvitedByUserName = x.AddedByName - }).ToArray(), - Members = members.Where(x => x.AccountId != 0).Select(x => new GetApplicationTeamMember - { - JoinedAtUtc = DateTime.UtcNow, - UserId = x.AccountId, - UserName = x.UserName - }).ToArray() - }; - return result; - } - } +using System; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Queries; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Applications.QueryHandlers +{ + internal class GetApplicationTeamHandler : IQueryHandler + { + private readonly IApplicationRepository _applicationRepository; + + public GetApplicationTeamHandler(IApplicationRepository applicationRepository) + { + _applicationRepository = applicationRepository; + } + + public async Task HandleAsync(IMessageContext context, GetApplicationTeam query) + { + var members = await _applicationRepository.GetTeamMembersAsync(query.ApplicationId); + var result = new GetApplicationTeamResult + { + Invited = members.Where(x => x.AccountId == 0).Select(x => new GetApplicationTeamResultInvitation + { + EmailAddress = x.EmailAddress, + InvitedAtUtc = x.AddedAtUtc, + InvitedByUserName = x.AddedByName + }).ToArray(), + Members = members.Where(x => x.AccountId != 0).Select(x => new GetApplicationTeamMember + { + JoinedAtUtc = DateTime.UtcNow, + UserId = x.AccountId, + UserName = x.UserName, + IsAdmin = x.Roles.Any(y => y == "Admin") + }).ToArray() + }; + return result; + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetMyApplications.cs b/src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetMyApplications.cs new file mode 100644 index 00000000..f4d5039e --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Applications/QueryHandlers/GetMyApplications.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications; +using Coderr.Server.Api.Core.Applications.Queries; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Applications.QueryHandlers +{ + /// + /// Handler for . + /// + public class GetApplicationListHandler : IQueryHandler + { + private readonly IApplicationRepository _applicationRepository; + private readonly IAccountRepository _accountRepository; + + /// + /// Creates a new instance of . + /// + /// repos + /// used to check if the user is sysadmin (can get all applications) + public GetApplicationListHandler(IApplicationRepository applicationRepository, + IAccountRepository accountRepository) + { + if (applicationRepository == null) throw new ArgumentNullException("applicationRepository"); + _applicationRepository = applicationRepository; + _accountRepository = accountRepository; + } + + /// + /// Method used to execute the query + /// + /// Query to execute. + /// + /// Task which will contain the result once completed. + /// + public async Task HandleAsync(IMessageContext context, GetApplicationList query) + { + if (query == null) throw new ArgumentNullException("query"); + ApplicationListItem[] result; + + var isSysAdmin = false; + if (query.AccountId > 0) + { + var account = await _accountRepository.GetByIdAsync((int)query.AccountId); + if (account.IsSysAdmin) + { + query.AccountId = 0; + isSysAdmin = true; + } + + } + + if (query.AccountId != 0) + { + var apps = await _applicationRepository.GetForUserAsync(query.AccountId); + result = ( + from x in apps + select new ApplicationListItem(x.ApplicationId, x.ApplicationName) + { + IsAdmin = x.IsAdmin, + NumberOfDevelopers = x.NumberOfDevelopers + } + ).ToArray(); + } + else + result = (await _applicationRepository.GetAllAsync()) + .Select(x => new ApplicationListItem(x.Id, x.Name) + { + IsAdmin = isSysAdmin, + NumberOfDevelopers = x.NumberOfFtes, + }) + .ToArray(); + + return result; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Environments/ApplicationEnvironment.cs b/src/Server/Coderr.Server.App/Core/Environments/ApplicationEnvironment.cs new file mode 100644 index 00000000..a0b38603 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Environments/ApplicationEnvironment.cs @@ -0,0 +1,55 @@ +using System; + +namespace Coderr.Server.App.Core.Environments +{ + /// + /// A mapping between an application and an environment. + /// + public class ApplicationEnvironment + { + public ApplicationEnvironment(int applicationId, int environmentId) + { + if (applicationId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(applicationId)); + } + + if (environmentId <= 0) + { + throw new ArgumentOutOfRangeException(nameof(environmentId)); + } + + ApplicationId = applicationId; + EnvironmentId = environmentId; + } + + protected ApplicationEnvironment() + { + } + + /// + /// Mapping for this application + /// + public int ApplicationId { get; private set; } + + /// + /// Do not track incidents in this environment. + /// + public bool DeleteIncidents { get; set; } + + /// + /// Environment that this is (from the Environments table),. + /// + public int EnvironmentId { get; private set; } + + /// + /// Primary key + /// + public int Id { get; set; } + + /// + /// Name as used when reporting errors (from environments table). + /// + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Environments/Environment.cs b/src/Server/Coderr.Server.App/Core/Environments/Environment.cs new file mode 100644 index 00000000..8069f5d9 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Environments/Environment.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Coderr.Server.App.Core.Environments +{ + /// + /// Keeps track of all named environments in a system. + /// + public class Environment + { + public Environment(string name) + { + Name = name; + } + + protected Environment() + { + + } + + /// + /// Primary key + /// + public int Id { get; set; } + + /// + /// Name as used when reporting errors. + /// + public string Name { get; private set; } + } +} diff --git a/src/Server/Coderr.Server.App/Core/Environments/Handlers/CreateEnvironmentHandler.cs b/src/Server/Coderr.Server.App/Core/Environments/Handlers/CreateEnvironmentHandler.cs new file mode 100644 index 00000000..59d68d08 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Environments/Handlers/CreateEnvironmentHandler.cs @@ -0,0 +1,37 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Environments.Commands; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Environments.Handlers +{ + internal class CreateEnvironmentHandler : IMessageHandler + { + private readonly IEnvironmentRepository _repository; + + public CreateEnvironmentHandler(IEnvironmentRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, CreateEnvironment message) + { + var env = await _repository.FindByName(message.Name); + if (env == null) + { + env = new Environment(message.Name); + await _repository.Create(env); + } + + var ae = await _repository.Find(env.Id, message.ApplicationId); + if (ae != null) + { + ae.DeleteIncidents = message.DeleteIncidents; + await _repository.Update(ae); + return; + } + + ae = new ApplicationEnvironment(message.ApplicationId, env.Id) {DeleteIncidents = message.DeleteIncidents}; + await _repository.Create(ae); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Environments/Handlers/GetEnvironmentsHandler.cs b/src/Server/Coderr.Server.App/Core/Environments/Handlers/GetEnvironmentsHandler.cs new file mode 100644 index 00000000..edae1142 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Environments/Handlers/GetEnvironmentsHandler.cs @@ -0,0 +1,26 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Environments.Queries; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Environments.Handlers +{ + public class GetEnvironmentsHandler : IQueryHandler + { + private readonly IEnvironmentRepository _repository; + + public GetEnvironmentsHandler(IEnvironmentRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, GetEnvironments query) + { + var items = (await _repository.ListForApplication(query.ApplicationId)) + .Select(x => new GetEnvironmentsResultItem { Name = x.Name, Id = x.EnvironmentId, DeleteIncidents = x.DeleteIncidents }) + .ToArray(); + + return new GetEnvironmentsResult { Items = items }; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Environments/Handlers/ResetEnvironmentHandler.cs b/src/Server/Coderr.Server.App/Core/Environments/Handlers/ResetEnvironmentHandler.cs new file mode 100644 index 00000000..8813fe40 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Environments/Handlers/ResetEnvironmentHandler.cs @@ -0,0 +1,25 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Environments.Commands; +using DotNetCqs; +using log4net; + +namespace Coderr.Server.App.Core.Environments.Handlers +{ + internal class ResetEnvironmentHandler : IMessageHandler + { + private readonly IEnvironmentRepository _environmentRepository; + private readonly ILog _loggr = LogManager.GetLogger(typeof(ResetEnvironmentHandler)); + + public ResetEnvironmentHandler(IEnvironmentRepository environmentRepository) + { + _environmentRepository = environmentRepository; + } + + public async Task HandleAsync(IMessageContext context, ResetEnvironment message) + { + _loggr.Info("Resetting environmentId " + message.EnvironmentId + " for app " + message.ApplicationId); + await _environmentRepository.Reset(message.EnvironmentId, + message.ApplicationId == 0 ? (int?)null : message.ApplicationId); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Environments/Handlers/UpdateEnvironmentHandler.cs b/src/Server/Coderr.Server.App/Core/Environments/Handlers/UpdateEnvironmentHandler.cs new file mode 100644 index 00000000..b5ea8a86 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Environments/Handlers/UpdateEnvironmentHandler.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Coderr.Client; +using Coderr.Server.Api.Core.Environments.Commands; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Environments.Handlers +{ + internal class UpdateEnvironmentHandler : IMessageHandler + { + private readonly IEnvironmentRepository _repository; + + public UpdateEnvironmentHandler(IEnvironmentRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, UpdateEnvironment message) + { + var appEnv = await _repository.Find(message.EnvironmentId, message.ApplicationId); + if (appEnv == null) + { + var env = await _repository.Find(message.EnvironmentId); + if (env == null) + { + Err.ReportLogicError("Failed to find environment " + message.EnvironmentId, + new + { + UserId = context.Principal.Identity.Name + }); + return; + } + + appEnv = new ApplicationEnvironment(message.ApplicationId, message.EnvironmentId) + { + Name = env.Name, + DeleteIncidents = message.DeleteIncidents + }; + } + else + { + appEnv.DeleteIncidents = message.DeleteIncidents; + } + + await _repository.Update(appEnv); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Environments/IEnvironmentRepository.cs b/src/Server/Coderr.Server.App/Core/Environments/IEnvironmentRepository.cs new file mode 100644 index 00000000..1aa61725 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Environments/IEnvironmentRepository.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Coderr.Server.App.Core.Environments +{ + public interface IEnvironmentRepository + { + Task Create(Environment environment); + Task Create(ApplicationEnvironment environment); + Task Delete(Environment environment); + Task Find(int environmentId, int applicationId); + Task Find(int environmentId); + Task FindByName(string name); + Task> ListAll(); + Task> ListForApplication(int applicationId); + Task Reset(int environmentId, int? applicationId); + Task Update(ApplicationEnvironment environment); + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/ErrorReports/Queries/FindReportHandler.cs b/src/Server/Coderr.Server.App/Core/ErrorReports/Queries/FindReportHandler.cs similarity index 87% rename from src/Server/OneTrueError.App/Core/ErrorReports/Queries/FindReportHandler.cs rename to src/Server/Coderr.Server.App/Core/ErrorReports/Queries/FindReportHandler.cs index 159f95b1..5cd43e43 100644 --- a/src/Server/OneTrueError.App/Core/ErrorReports/Queries/FindReportHandler.cs +++ b/src/Server/Coderr.Server.App/Core/ErrorReports/Queries/FindReportHandler.cs @@ -1,80 +1,80 @@ -//using System; -//using System.Linq; -//using System.Threading.Tasks; -//using DotNetCqs; -//using Griffin.Container; -//using log4net; -//using OneTrueError.Core.Api.Reports; -//using OneTrueError.Core.Api.Reports.Queries; -//using OneTrueError.Core.Reports; - -//namespace OneTrueError.Core.ErrorReports.Queries -//{ -// [Component] -// internal class FindReportHandler : IQueryHandler -// { -// private readonly IReportsRepository _repository; -// private ILog _logger = LogManager.GetLogger(typeof (FindReportHandler)); - -// public FindReportHandler(IReportsRepository repository) -// { -// _repository = repository; -// } - -// public async Task ExecuteAsync(GetReport query) -// { -// ErrorReportEntity entity = null; - -// if (!string.IsNullOrEmpty(query.ErrorId)) -// { -// entity = await _repository.FindByErrorIdAsync(query.ErrorId); -// } -// if (entity == null && query.ReportId != 0) -// { -// entity = await _repository.GetAsync(query.ReportId); -// } - - -// if (entity == null) -// { -// _logger.ErrorFormat("Failed to find report for {0} / {1}", query.ErrorId, query.ReportId); -// return null; -// } - - -// return new ReportDTO -// { -// ApplicationId = entity.ApplicationId, -// ContextCollections = entity.ContextInfo.Select(ConvertCollection).ToArray(), -// CreatedAtUtc = entity.CreatedAtUtc, -// Exception = ConvertException(entity.Exception), -// Id = entity.Id.ToString(), -// IncidentId = entity.IncidentId, -// RemoteAddress = entity.RemoteAddress, -// ReportVersion = "1" -// }; -// } - -// private NewReportException ConvertException(ErrorReportException exception) -// { -// return new NewReportException -// { -// AssemblyName = exception.AssemblyName, -// BaseClasses = exception.BaseClasses, -// Everything = exception.Everything, -// FullName = exception.FullName, -// InnerException = exception.InnerException == null ? null : ConvertException(exception.InnerException), -// Message = exception.Message, -// Name = exception.Name, -// Namespace = exception.Namespace, -// StackTrace = exception.StackTrace -// }; -// } - -// private NewReportContextInfo ConvertCollection(ErrorReportContext arg) -// { -// return new NewReportContextInfo(arg.Name, arg.Properties); -// } -// } -//} - +//using System; +//using System.Linq; +//using System.Threading.Tasks; +//using DotNetCqs; +// +//using log4net; +//using Coderr.Core.Api.Reports; +//using Coderr.Core.Api.Reports.Queries; +//using Coderr.Core.Reports; + +//namespace Coderr.Core.ErrorReports.Queries +//{ +// [ContainerService] +// internal class FindReportHandler : IQueryHandler +// { +// private readonly IReportsRepository _repository; +// private ILog _logger = LogManager.GetLogger(typeof (FindReportHandler)); + +// public FindReportHandler(IReportsRepository repository) +// { +// _repository = repository; +// } + +// public async Task ExecuteAsync(IMessageContext context, GetReport query) +// { +// ErrorReportEntity entity = null; + +// if (!string.IsNullOrEmpty(query.ErrorId)) +// { +// entity = await _repository.FindByErrorIdAsync(query.ErrorId); +// } +// if (entity == null && query.ReportId != 0) +// { +// entity = await _repository.GetAsync(query.ReportId); +// } + + +// if (entity == null) +// { +// _logger.ErrorFormat("Failed to find report for {0} / {1}", query.ErrorId, query.ReportId); +// return null; +// } + + +// return new ReportDTO +// { +// ApplicationId = entity.ApplicationId, +// ContextCollections = entity.ContextInfo.Select(ConvertCollection).ToArray(), +// CreatedAtUtc = entity.CreatedAtUtc, +// Exception = ConvertException(entity.Exception), +// Id = entity.Id.ToString(), +// IncidentId = entity.IncidentId, +// RemoteAddress = entity.RemoteAddress, +// ReportVersion = "1" +// }; +// } + +// private NewReportException ConvertException(ErrorReportException exception) +// { +// return new NewReportException +// { +// AssemblyName = exception.AssemblyName, +// BaseClasses = exception.BaseClasses, +// Everything = exception.Everything, +// FullName = exception.FullName, +// InnerException = exception.InnerException == null ? null : ConvertException(exception.InnerException), +// Message = exception.Message, +// Name = exception.Name, +// Namespace = exception.Namespace, +// StackTrace = exception.StackTrace +// }; +// } + +// private NewReportContextInfo ConvertCollection(ErrorReportContext arg) +// { +// return new NewReportContextInfo(arg.Name, arg.Properties); +// } +// } +//} + diff --git a/src/Server/Coderr.Server.App/Core/Incidents/Commands/AssignIncidentHandler.cs b/src/Server/Coderr.Server.App/Core/Incidents/Commands/AssignIncidentHandler.cs new file mode 100644 index 00000000..2674b799 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Incidents/Commands/AssignIncidentHandler.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Incidents.Commands; +using Coderr.Server.Api.Core.Incidents.Events; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Infrastructure.Security; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Incidents.Commands +{ + /// + /// Handler for + /// + public class AssignIncidentHandler : IMessageHandler + { + private readonly IIncidentRepository _repository; + + public AssignIncidentHandler(IIncidentRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, AssignIncident message) + { + var assignedBy = message.AssignedBy; + if (assignedBy == 0) + assignedBy = context.Principal.GetAccountId(); + if (message.AssignedAtUtc == DateTime.MinValue) + message.AssignedAtUtc = null; + + var incident = await _repository.GetAsync(message.IncidentId); + incident.Assign(message.AssignedTo, message.AssignedAtUtc); + await _repository.UpdateAsync(incident); + + var evt = new IncidentAssigned(message.IncidentId, assignedBy, message.AssignedTo, message.AssignedAtUtc ?? DateTime.UtcNow); + await context.SendAsync(evt); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Incidents/Commands/CloseIncidentHandler.cs b/src/Server/Coderr.Server.App/Core/Incidents/Commands/CloseIncidentHandler.cs new file mode 100644 index 00000000..c73ce349 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Incidents/Commands/CloseIncidentHandler.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Commands; +using Coderr.Server.Api.Core.Incidents.Events; +using Coderr.Server.Api.Core.Messaging; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Domain.Core.Feedback; +using Coderr.Server.Domain.Core.Incidents; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Incidents.Commands +{ + /// + /// Handler of . + /// + public class CloseIncidentHandler : IMessageHandler + { + private readonly IFeedbackRepository _feedbackRepository; + private readonly IIncidentRepository _repository; + + /// + /// Creates a new instance of . + /// + /// To be able to load and update incident + /// To be able to see if someone is waiting on update notifications. + public CloseIncidentHandler(IIncidentRepository repository, IFeedbackRepository feedbackRepository) + { + _repository = repository; + _feedbackRepository = feedbackRepository; + } + + /// + /// Execute a command asynchronously. + /// + /// Command to execute. + /// + /// Task which will be completed once the command has been executed. + /// + public async Task HandleAsync(IMessageContext context, CloseIncident command) + { + if (command == null) throw new ArgumentNullException("command"); + + //TODO: Add latest version if not specified. + + var incident = await _repository.GetAsync(command.IncidentId); + incident.Close(command.UserId, command.Solution, command.ApplicationVersion); + if (command.ShareSolution) + incident.ShareSolution(); + + if (command.CanSendNotification && !string.IsNullOrEmpty(command.NotificationTitle) && + !string.IsNullOrEmpty(command.NotificationText)) + { + var emails = await _feedbackRepository.GetEmailAddressesAsync(command.IncidentId); + if (emails.Distinct().Any()) + { + var emailMessage = new EmailMessage(emails) + { + Subject = command.NotificationTitle, + TextBody = command.NotificationText + }; + var sendMessage = new SendEmail(emailMessage); + await context.SendAsync(sendMessage); + } + } + + await _repository.UpdateAsync(incident); + + var closedEvt = + new IncidentClosed(incident.ApplicationId, incident.Id, command.UserId, command.Solution, command.ApplicationVersion, command.ClosedAtUtc ?? DateTime.UtcNow); + await context.SendAsync(closedEvt); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Incidents/Commands/DeleteIncidentHandler.cs b/src/Server/Coderr.Server.App/Core/Incidents/Commands/DeleteIncidentHandler.cs new file mode 100644 index 00000000..ddfcc371 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Incidents/Commands/DeleteIncidentHandler.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Incidents.Commands; +using Coderr.Server.Api.Core.Incidents.Events; +using Coderr.Server.Domain.Core.Incidents; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Incidents.Commands +{ + /// + /// Handler for + /// + public class DeleteIncidentHandler : IMessageHandler + { + private readonly IIncidentRepository _repository; + + public DeleteIncidentHandler(IIncidentRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, DeleteIncident message) + { + var incident = await _repository.GetAsync(message.IncidentId); + await _repository.Delete(message.IncidentId); + + var evt = new IncidentDeleted(message.IncidentId, message.UserId ?? context.Principal.GetAccountId(), + message.DeletedAtUtc ?? DateTime.UtcNow); + await context.SendAsync(evt); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Incidents/Commands/IgnoreIncidentHandler.cs b/src/Server/Coderr.Server.App/Core/Incidents/Commands/IgnoreIncidentHandler.cs new file mode 100644 index 00000000..eceed5a2 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Incidents/Commands/IgnoreIncidentHandler.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Commands; +using Coderr.Server.Api.Core.Incidents.Events; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Core.User; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Incidents.Commands +{ + /// + /// Handler for . + /// + public class IgnoreIncidentHandler : IMessageHandler + { + private readonly IIncidentRepository _incidentRepository; + private readonly IUserRepository _userRepository; + + /// + /// Creates a new instance of . + /// + /// to load and update info about the incident that is being ignored + /// to get info about the user that ignores the incident + /// + public IgnoreIncidentHandler(IIncidentRepository incidentRepository, IUserRepository userRepository) + { + if (incidentRepository == null) throw new ArgumentNullException("incidentRepository"); + if (userRepository == null) throw new ArgumentNullException("userRepository"); + + _incidentRepository = incidentRepository; + _userRepository = userRepository; + } + + /// Execute a command asynchronously. + /// Command to execute. + /// Task which will be completed once the command has been executed. + public async Task HandleAsync(IMessageContext context, IgnoreIncident command) + { + var user = await _userRepository.GetUserAsync(command.UserId); + var incident = await _incidentRepository.GetAsync(command.IncidentId); + incident.IgnoreFutureReports(user.UserName); + await _incidentRepository.UpdateAsync(incident); + + await context.SendAsync(new IncidentIgnored(incident.ApplicationId, command.IncidentId, user.AccountId, user.UserName)); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Incidents/Commands/NotifySubscribersHandler.cs b/src/Server/Coderr.Server.App/Core/Incidents/Commands/NotifySubscribersHandler.cs new file mode 100644 index 00000000..0175c975 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Incidents/Commands/NotifySubscribersHandler.cs @@ -0,0 +1,36 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Commands; +using Coderr.Server.Api.Core.Messaging; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Domain.Core.Feedback; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Incidents.Commands +{ + internal class NotifySubscribersHandler : IMessageHandler + { + private readonly IFeedbackRepository _feedbackRepository; + + public NotifySubscribersHandler(IFeedbackRepository feedbackRepository) + { + _feedbackRepository = feedbackRepository; + } + + + public async Task HandleAsync(IMessageContext context, NotifySubscribers message) + { + var emails = await _feedbackRepository.GetEmailAddressesAsync(message.IncidentId); + if (!emails.Any()) + return; + + var emailMessage = new EmailMessage(emails) + { + Subject = message.Title, + TextBody = message.Body + }; + var sendMessage = new SendEmail(emailMessage); + await context.SendAsync(sendMessage); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Incidents/Commands/ReOpenIncidentHandler.cs b/src/Server/Coderr.Server.App/Core/Incidents/Commands/ReOpenIncidentHandler.cs new file mode 100644 index 00000000..ba9c1e45 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Incidents/Commands/ReOpenIncidentHandler.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Commands; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Core.Incidents.Events; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Incidents.Commands +{ + /// + /// Uses the incident repository and the domain entity to apply the change. + /// + public class ReOpenIncidentHandler : IMessageHandler + { + private readonly IIncidentRepository _repository; + + /// + /// Creates a new instance of . + /// + /// To be able to load and update incident + public ReOpenIncidentHandler(IIncidentRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + + /// + public async Task HandleAsync(IMessageContext context, ReOpenIncident command) + { + var incident = await _repository.GetAsync(command.IncidentId); + incident.Reopen(); + await _repository.UpdateAsync(incident); + + var evt = new IncidentReOpened(incident.ApplicationId, incident.Id, DateTime.UtcNow); + await context.SendAsync(evt); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Incidents/Jobs/DeleteOldIncidents.cs b/src/Server/Coderr.Server.App/Core/Incidents/Jobs/DeleteOldIncidents.cs new file mode 100644 index 00000000..b7e750d9 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Incidents/Jobs/DeleteOldIncidents.cs @@ -0,0 +1,104 @@ +using System; +using System.Data; +using System.Diagnostics; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Abstractions.Reports; +using Griffin.ApplicationServices; +using Griffin.Data; +using Griffin.Data.Mapper; +using log4net; + +namespace Coderr.Server.App.Core.Incidents.Jobs +{ + /// + /// Delete incidents where all reports have been deleted (due to retention days). + /// + /// + /// + /// There are other jobs where old reports are removed. This job is to make sure that old incidents are being + /// deleted + /// when there are no reports for them. Do note that ignored incidents will not be deleted. + /// + /// + [ContainerService(RegisterAsSelf = true)] + internal class DeleteOldIncidents : IBackgroundJobAsync + { + private readonly ILog _logger = LogManager.GetLogger(typeof(DeleteOldIncidents)); + private readonly IDbConnection _connection; + private readonly IConfiguration _reportConfiguration; + + /// + /// Creates a new instance of . + /// + /// Used for SQL queries + public DeleteOldIncidents(IDbConnection connection, IConfiguration reportConfiguration) + { + _connection = connection; + _reportConfiguration = reportConfiguration; + } + + /// + public async Task ExecuteAsync() + { + if (HostConfig.Instance.IsDemo) + return; + + using (var cmd = _connection.CreateDbCommand()) + { + cmd.CommandText = + $@"CREATE TABLE #ItemsToDelete + ( + Id int NOT NULL PRIMARY KEY + ) + + INSERT #ItemsToDelete (Id) + SELECT TOP(500) Id + FROM Incidents WITH (ReadPast) + WHERE CreatedAtUtc < @retentionDays AND Incidents.State = 0 + declare @counter int = 0; + + IF @@ROWCOUNT <> 0 + BEGIN + DECLARE ItemsToDeleteCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY + FOR SELECT Id FROM #ItemsToDelete + set @counter = 1 + + DECLARE @IdToDelete int + OPEN ItemsToDeleteCursor + FETCH NEXT FROM ItemsToDeleteCursor INTO @IdToDelete + + WHILE @@FETCH_STATUS = 0 + BEGIN + set @counter = @counter + 1 + DELETE FROM Incidents WHERE Id = @IdToDelete + FETCH NEXT FROM ItemsToDeleteCursor INTO @IdToDelete + END + + CLOSE ItemsToDeleteCursor + DEALLOCATE ItemsToDeleteCursor + END + DROP TABLE #ItemsToDelete + select @counter;"; + + // OLD: + // Wait until no reports have been received for the specified report save time + // and then make sure during another period that no new reports have been received. + // + // NEW: + // We'll delete incidents when they was created long time ago and no one have started to work on them. + // In that way, those that still are happening will be brought to top, and all aggregated data will be cleaned. + var incidentRetention = _reportConfiguration.Value.RetentionDaysIncidents * 2; + + cmd.AddParameter("retentionDays", DateTime.Today.AddDays(-incidentRetention)); + var rows = await cmd.ExecuteNonQueryAsync(); + if (rows > 0) + { + _logger.Debug("Deleted " + rows + " empty incidents."); + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Invitations/ApplicationInvitation.cs b/src/Server/Coderr.Server.App/Core/Invitations/ApplicationInvitation.cs similarity index 91% rename from src/Server/OneTrueError.App/Core/Invitations/ApplicationInvitation.cs rename to src/Server/Coderr.Server.App/Core/Invitations/ApplicationInvitation.cs index 7d226d32..34d3a808 100644 --- a/src/Server/OneTrueError.App/Core/Invitations/ApplicationInvitation.cs +++ b/src/Server/Coderr.Server.App/Core/Invitations/ApplicationInvitation.cs @@ -1,25 +1,25 @@ -using System; - -namespace OneTrueError.App.Core.Invitations -{ - /// - /// Invitation to a specific application. - /// - public class ApplicationInvitation - { - /// - /// The application that this invitation is for when it comes to access of the application. - /// - public int ApplicationId { get; set; } - - /// - /// When the invitation was created, skipping shit like daylight faking time and time stones. - /// - public DateTime InvitedAtUtc { get; set; } - - /// - /// Username of the user that invited the user user for the application that both uses. - /// - public string InvitedBy { get; set; } - } +using System; + +namespace Coderr.Server.App.Core.Invitations +{ + /// + /// Invitation to a specific application. + /// + public class ApplicationInvitation + { + /// + /// The application that this invitation is for when it comes to access of the application. + /// + public int ApplicationId { get; set; } + + /// + /// When the invitation was created, skipping shit like daylight faking time and time stones. + /// + public DateTime InvitedAtUtc { get; set; } + + /// + /// Username of the user that invited the user user for the application that both uses. + /// + public string InvitedBy { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs new file mode 100644 index 00000000..f3df184c --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Requests; +using Coderr.Server.App.Core.Accounts; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Invitations.CommandHandlers +{ + public class AcceptInvitationHandler : IMessageHandler + { + private readonly IAccountService _accountService; + + public AcceptInvitationHandler(IAccountService accountService) + { + _accountService = accountService; + } + + public async Task HandleAsync(IMessageContext context, AcceptInvitation message) + { + await _accountService.AcceptInvitation(context.Principal, message); + } + } +} diff --git a/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/DeleteInvitationHandler.cs b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/DeleteInvitationHandler.cs new file mode 100644 index 00000000..d0cf341e --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/DeleteInvitationHandler.cs @@ -0,0 +1,45 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Invitations.Commands; +using Coderr.Server.Api.Core.Invitations.Events; +using Coderr.Server.Domain.Core.Applications; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Invitations.CommandHandlers +{ + internal class DeleteInvitationHandler : IMessageHandler + { + private readonly IApplicationRepository _applicationRepository; + private readonly IInvitationRepository _invitationRepository; + + public DeleteInvitationHandler(IApplicationRepository applicationRepository, + IInvitationRepository invitationRepository) + { + _applicationRepository = applicationRepository; + _invitationRepository = invitationRepository; + } + + public async Task HandleAsync(IMessageContext context, DeleteInvitation message) + { + var invite = await _invitationRepository.FindByEmailAsync(message.InvitedEmailAddress); + await _applicationRepository.RemoveTeamMemberAsync(message.ApplicationId, message.InvitedEmailAddress); + + invite.Remove(message.ApplicationId); + if (!invite.Invitations.Any()) + { + await _invitationRepository.DeleteAsync(invite.InvitationKey); + await context.SendAsync(new InvitationDeleted + { + ApplicationIds = new[] {message.ApplicationId}, + InvitedEmailAddress = message.InvitedEmailAddress, + InvitationId = invite.Id + }); + } + + else + { + await _invitationRepository.UpdateAsync(invite); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs new file mode 100644 index 00000000..d56fd015 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs @@ -0,0 +1,168 @@ +using System.Linq; +using System.Security; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Applications.Events; +using Coderr.Server.Api.Core.Invitations.Commands; +using Coderr.Server.Api.Core.Messaging; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Domain.Core.User; +using Coderr.Server.Infrastructure.Configuration; +using DotNetCqs; + +using log4net; + +namespace Coderr.Server.App.Core.Invitations.CommandHandlers +{ + /// + /// Handler for + /// + /// + /// + /// + /// + /// + public class InviteUserHandler : IMessageHandler + { + private readonly IApplicationRepository _applicationRepository; + private readonly IInvitationRepository _invitationRepository; + private readonly IUserRepository _userRepository; + private readonly ILog _logger = LogManager.GetLogger(typeof(InviteUserHandler)); + private readonly BaseConfiguration _baseConfiguration; + + /// + /// Creates a new instance of . + /// + /// Store invitations. + /// To load invited and invitee. + /// Add pending member. + /// To get the base url. + public InviteUserHandler(IInvitationRepository invitationRepository, + IUserRepository userRepository, IApplicationRepository applicationRepository, IConfiguration baseConfig) + { + _invitationRepository = invitationRepository; + _userRepository = userRepository; + _applicationRepository = applicationRepository; + _baseConfiguration = baseConfig.Value; + } + + /// + public async Task HandleAsync(IMessageContext context, InviteUser command) + { + var inviter = await _userRepository.GetUserAsync(command.UserId); + if (!context.Principal.IsSysAdmin() && + !context.Principal.IsApplicationAdmin(command.ApplicationId)) + { + _logger.Warn($"User {command.UserId} attempted to do an invite for an application: {command.ApplicationId}."); + throw new SecurityException("You are not an admin of that application."); + } + + + var invitedUser = await _userRepository.FindByEmailAsync(command.EmailAddress); + if (invitedUser != null) + { + //correction of issue #21, verify that the person isn't already a member. + var members = await _applicationRepository.GetTeamMembersAsync(command.ApplicationId); + if (members.Any(x => x.AccountId == invitedUser.AccountId)) + { + _logger.Warn("User " + invitedUser.AccountId + " is already a member."); + return; + } + + var member = new ApplicationTeamMember(command.ApplicationId, invitedUser.AccountId, inviter.UserName) + { + Roles = new[] { ApplicationRole.Member } + }; + + await _applicationRepository.CreateAsync(member); + await context.SendAsync(new UserAddedToApplication(command.ApplicationId, member.AccountId)); + return; + } + else + { + //correction of issue #21, verify that the person isn't already a member. + var members = await _applicationRepository.GetTeamMembersAsync(command.ApplicationId); + if (members.Any(x => x.EmailAddress == command.EmailAddress)) + { + _logger.Warn("User " + command.EmailAddress + " is already invited."); + return; + } + } + + var invitedMember = new ApplicationTeamMember(command.ApplicationId, command.EmailAddress) + { + AddedByName = inviter.UserName, + Roles = new[] { ApplicationRole.Member } + }; + await _applicationRepository.CreateAsync(invitedMember); + var invitation = await _invitationRepository.FindByEmailAsync(command.EmailAddress); + if (invitation == null) + { + invitation = new Invitation(command.EmailAddress, inviter.UserName); + await _invitationRepository.CreateAsync(invitation); + await SendInvitationEmailAsync(context, invitation, command.Text); + } + + invitation.Add(command.ApplicationId, inviter.UserName); + await _invitationRepository.UpdateAsync(invitation); + + var app = await _applicationRepository.GetByIdAsync(command.ApplicationId); + var evt = new UserInvitedToApplication( + invitation.InvitationKey, + command.ApplicationId, + app.Name, + command.EmailAddress, + inviter.UserName); + + await context.SendAsync(evt); + } + + /// + /// Send invitation email + /// + /// Invitation to generate an email for + /// Why the user was invited (optional) + /// task + protected virtual async Task SendInvitationEmailAsync(IMessageContext context, Invitation invitation, string reason) + { + var url = _baseConfiguration.BaseUrl.ToString().TrimEnd('/'); + + + if (ServerConfig.Instance.IsLive) + url = url.Replace("/app.", "/lobby."); + + if (string.IsNullOrEmpty(reason)) + reason = ""; + else + reason += "\r\n"; + + var inviteUrl = $"https://lobby.coderr.io/invitation/accept/{invitation.InvitationKey}"; + if (!ServerConfig.Instance.IsLive) + { + inviteUrl = $"{_baseConfiguration.BaseUrl}account/accept/{invitation.InvitationKey}"; + } + var msg = new EmailMessage + { + Subject = "You have been invited by " + invitation.InvitedBy + " to Coderr.", + TextBody = $@"Hello, + +{invitation.InvitedBy} has invited to you join their team at Coderr, a service used to keep track of exceptions in .NET applications. + +Click on the following link to accept the invitation: +{inviteUrl} + +{reason} + +Best regards, + The Coderr team +", + Recipients = new[] { new EmailAddress(invitation.EmailToInvitedUser) } + }; + + await context.SendAsync(new SendEmail(msg)); + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Invitations/IInvitationRepository.cs b/src/Server/Coderr.Server.App/Core/Invitations/IInvitationRepository.cs similarity index 94% rename from src/Server/OneTrueError.App/Core/Invitations/IInvitationRepository.cs rename to src/Server/Coderr.Server.App/Core/Invitations/IInvitationRepository.cs index b9d3a3a4..f6dc3e1c 100644 --- a/src/Server/OneTrueError.App/Core/Invitations/IInvitationRepository.cs +++ b/src/Server/Coderr.Server.App/Core/Invitations/IInvitationRepository.cs @@ -1,50 +1,50 @@ -using System; -using System.Threading.Tasks; - -namespace OneTrueError.App.Core.Invitations -{ - /// - /// Invitation repository - /// - public interface IInvitationRepository - { - /// - /// Create a new invitation - /// - /// invitation - /// task - /// invitation - Task CreateAsync(Invitation invitation); - - /// - /// Delete invitation - /// - /// Key that was sent out in the invitation email - /// task - /// invitationKey - Task DeleteAsync(string invitationKey); - - /// - /// Find invitation by email - /// - /// email for the invited user. - /// invitation if found; otherwise null. - /// email - Task FindByEmailAsync(string email); - - /// - /// Get invitation by key. - /// - /// Key sent out in the invitation email. - /// Invitation - Task GetByInvitationKeyAsync(string invitationKey); - - /// - /// Update existing invitation - /// - /// invitation - /// task - /// invitation - Task UpdateAsync(Invitation invitation); - } +using System; +using System.Threading.Tasks; + +namespace Coderr.Server.App.Core.Invitations +{ + /// + /// Invitation repository + /// + public interface IInvitationRepository + { + /// + /// Create a new invitation + /// + /// invitation + /// task + /// invitation + Task CreateAsync(Invitation invitation); + + /// + /// Delete invitation + /// + /// Key that was sent out in the invitation email + /// task + /// invitationKey + Task DeleteAsync(string invitationKey); + + /// + /// Find invitation by email + /// + /// email for the invited user. + /// invitation if found; otherwise null. + /// email + Task FindByEmailAsync(string email); + + /// + /// Get invitation by key. + /// + /// Key sent out in the invitation email. + /// Invitation + Task GetByInvitationKeyAsync(string invitationKey); + + /// + /// Update existing invitation + /// + /// invitation + /// task + /// invitation + Task UpdateAsync(Invitation invitation); + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Invitations/Invitation.cs b/src/Server/Coderr.Server.App/Core/Invitations/Invitation.cs similarity index 83% rename from src/Server/OneTrueError.App/Core/Invitations/Invitation.cs rename to src/Server/Coderr.Server.App/Core/Invitations/Invitation.cs index 36487048..daf2f82e 100644 --- a/src/Server/OneTrueError.App/Core/Invitations/Invitation.cs +++ b/src/Server/Coderr.Server.App/Core/Invitations/Invitation.cs @@ -1,92 +1,102 @@ -using System; -using System.Collections.Generic; - -namespace OneTrueError.App.Core.Invitations -{ - /// - /// Invitation to join this OTE installation - /// - public class Invitation - { - /// - /// Creates a new instance of . - /// - /// Email to the user that is not a member yet - /// Username for the one that made the invitation. - /// emailToInvitedUser; userNameForInviter - public Invitation(string emailToInvitedUser, string userNameForInviter) - { - if (emailToInvitedUser == null) throw new ArgumentNullException("emailToInvitedUser"); - if (userNameForInviter == null) throw new ArgumentNullException("userNameForInviter"); - - EmailToInvitedUser = emailToInvitedUser; - InvitedBy = userNameForInviter; - InvitationKey = Guid.NewGuid().ToString("N"); - CreatedAtUtc = DateTime.UtcNow; - Invitations = new List(); - } - - /// - /// Serialization constructor - /// - protected Invitation() - { - Invitations = new List(); - } - - /// - /// When the invitation request was created - /// - public DateTime CreatedAtUtc { get; private set; } - - /// - /// Email to the user that was invited. - /// - public string EmailToInvitedUser { get; private set; } - - /// - /// Id - /// - public int Id { get; private set; } - - /// - /// Used when clicking on the invitation link to identify this specific invitation - /// - public string InvitationKey { get; private set; } - - /// - /// All applications that the user will get membership to once accepting the invitation. - /// - /// - /// - /// A new item is created for every application that the user is invited to. - /// - /// - public IList Invitations { get; private set; } - - /// - /// Username of the user that sent the invitation - /// - public string InvitedBy { get; private set; } - - /// - /// Add a mapping for another application (user will gain access to all applications once the invitation is accepted). - /// - /// application id - /// user that made this invitation - /// invitedByUser - /// applicationId - public void Add(int applicationId, string invitedByUser) - { - if (invitedByUser == null) throw new ArgumentNullException("invitedByUser"); - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - - Invitations.Add(new ApplicationInvitation - { - ApplicationId = applicationId, - InvitedBy = invitedByUser, - InvitedAtUtc = DateTime.UtcNow - }); - } - } +using System; +using System.Collections.Generic; + +namespace Coderr.Server.App.Core.Invitations +{ + /// + /// Invitation to join this OTE installation + /// + public class Invitation + { + private List _invitations = new List(); + + /// + /// Creates a new instance of . + /// + /// Email to the user that is not a member yet + /// Username for the one that made the invitation. + /// emailToInvitedUser; userNameForInviter + public Invitation(string emailToInvitedUser, string userNameForInviter) + { + if (emailToInvitedUser == null) throw new ArgumentNullException("emailToInvitedUser"); + if (userNameForInviter == null) throw new ArgumentNullException("userNameForInviter"); + + EmailToInvitedUser = emailToInvitedUser; + InvitedBy = userNameForInviter; + InvitationKey = Guid.NewGuid().ToString("N"); + CreatedAtUtc = DateTime.UtcNow; + } + + /// + /// Serialization constructor + /// + protected Invitation() + { + } + + /// + /// When the invitation request was created + /// + public DateTime CreatedAtUtc { get; private set; } + + /// + /// Email to the user that was invited. + /// + public string EmailToInvitedUser { get; private set; } + + /// + /// Id + /// + public int Id { get; private set; } + + /// + /// Used when clicking on the invitation link to identify this specific invitation + /// + public string InvitationKey { get; private set; } + + /// + /// All applications that the user will get membership to once accepting the invitation. + /// + /// + /// + /// A new item is created for every application that the user is invited to. + /// + /// + public IEnumerable Invitations + { + get { return _invitations; } + private set { _invitations = new List(value); } + } + + /// + /// Username of the user that sent the invitation + /// + public string InvitedBy { get; private set; } + + /// + /// Add a mapping for another application (user will gain access to all applications once the invitation is accepted). + /// + /// application id + /// user that made this invitation + /// invitedByUser + /// applicationId + public void Add(int applicationId, string invitedByUser) + { + if (invitedByUser == null) throw new ArgumentNullException("invitedByUser"); + if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); + + _invitations.Add(new ApplicationInvitation + { + ApplicationId = applicationId, + InvitedBy = invitedByUser, + InvitedAtUtc = DateTime.UtcNow + }); + } + + public void Remove(int applicationId) + { + if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); + _invitations.RemoveAll(x => x.ApplicationId == applicationId); + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Notifications/Commands/AddNotificationHandler.cs b/src/Server/Coderr.Server.App/Core/Notifications/Commands/AddNotificationHandler.cs new file mode 100644 index 00000000..b9cd6af9 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Notifications/Commands/AddNotificationHandler.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Notifications; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Notifications.Commands +{ + /// + /// Handler for . + /// + public class AddNotificationHandler : IMessageHandler + { + /// + /// Not implemented yet. + /// + /// cmd + /// task + public Task HandleAsync(IMessageContext context, AddNotification command) + { + //TODO: Implement + return Task.FromResult(null); + } + } +} diff --git a/src/Server/Coderr.Server.App/Core/Notifications/Commands/DeleteBrowserSubscriptionHandler.cs b/src/Server/Coderr.Server.App/Core/Notifications/Commands/DeleteBrowserSubscriptionHandler.cs new file mode 100644 index 00000000..315ccce6 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Notifications/Commands/DeleteBrowserSubscriptionHandler.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Users.Commands; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Notifications.Commands +{ + internal class DeleteBrowserSubscriptionHandler : IMessageHandler + { + private readonly INotificationsRepository _repository; + + public DeleteBrowserSubscriptionHandler(INotificationsRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, DeleteBrowserSubscription message) + { + await _repository.DeleteBrowserSubscription(message.UserId, message.Endpoint); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Notifications/Commands/StoreBrowserSubscriptionHandler.cs b/src/Server/Coderr.Server.App/Core/Notifications/Commands/StoreBrowserSubscriptionHandler.cs new file mode 100644 index 00000000..242f55d5 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Notifications/Commands/StoreBrowserSubscriptionHandler.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Users.Commands; +using Coderr.Server.Domain.Modules.UserNotifications; +using DotNetCqs; + +namespace Coderr.Server.App.Core.Notifications.Commands +{ + internal class StoreBrowserSubscriptionHandler : IMessageHandler + { + private readonly INotificationsRepository _notificationsRepository; + + public StoreBrowserSubscriptionHandler(INotificationsRepository notificationsRepository) + { + _notificationsRepository = notificationsRepository; + } + + public async Task HandleAsync(IMessageContext context, StoreBrowserSubscription message) + { + var subscription = new BrowserSubscription + { + AccountId = context.Principal.GetAccountId(), + AuthenticationSecret = message.AuthenticationSecret, + Endpoint = message.Endpoint, + PublicKey = message.PublicKey, + ExpiresAtUtc = message.ExpirationTime == null + ? (DateTime?)null + : new DateTime(1970, 1, 1).AddMilliseconds(message.ExpirationTime.Value) + }; + await _notificationsRepository.Save(subscription); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Notifications/EventHandlers/ApplicationDeletedHandler.cs b/src/Server/Coderr.Server.App/Core/Notifications/EventHandlers/ApplicationDeletedHandler.cs new file mode 100644 index 00000000..f1822334 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Notifications/EventHandlers/ApplicationDeletedHandler.cs @@ -0,0 +1,33 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Applications.Events; +using DotNetCqs; + +using Griffin.Data; + +namespace Coderr.Server.App.Core.Notifications.EventHandlers +{ + /// + /// Will delete all reports for the given application + /// + public class ApplicationDeletedHandler : IMessageHandler + { + private IAdoNetUnitOfWork _uow; + + /// + /// Creates a new instance of . + /// + public ApplicationDeletedHandler(IAdoNetUnitOfWork uow) + { + if (uow == null) throw new ArgumentNullException("uow"); + _uow = uow; + } + + /// + public Task HandleAsync(IMessageContext context, ApplicationDeleted e) + { + _uow.ExecuteNonQuery("DELETE FROM UserNotificationSettings WHERE ApplicationId = @id", new { id = e.ApplicationId }); + return Task.FromResult(null); + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Notifications/INotificationsRepository.cs b/src/Server/Coderr.Server.App/Core/Notifications/INotificationsRepository.cs similarity index 78% rename from src/Server/OneTrueError.App/Core/Notifications/INotificationsRepository.cs rename to src/Server/Coderr.Server.App/Core/Notifications/INotificationsRepository.cs index 55270e68..2a1783cb 100644 --- a/src/Server/OneTrueError.App/Core/Notifications/INotificationsRepository.cs +++ b/src/Server/Coderr.Server.App/Core/Notifications/INotificationsRepository.cs @@ -1,57 +1,53 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; - -namespace OneTrueError.App.Core.Notifications -{ - /// - /// Repository for notification settings - /// - public interface INotificationsRepository - { - /// - /// Create settings - /// - /// settings - /// task - /// notificationSettings - Task CreateAsync(UserNotificationSettings notificationSettings); - - /// - /// Check if there are any settings for the given application and the specified user. - /// - /// user - /// application - /// true if settings exists; otherwise null. - Task ExistsAsync(int accountId, int applicationId); - - - /// - /// Get application settings for all users. - /// - /// applicationId - /// Default setting will be returned for users that do not have any application specific. - /// applicationId - [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] - [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] - Task> GetAllAsync(int applicationId); - - /// - /// Get settings - /// - /// account that want to modify it's settings - /// Application to modify settings for (0 = default settings) - /// settings if found; otherwise null. - /// accountId; applicationId - Task TryGetAsync(int accountId, int applicationId); - - /// - /// Update notifications - /// - /// settings - /// task - /// notificationSettings - Task UpdateAsync(UserNotificationSettings notificationSettings); - } +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Users.Commands; +using Coderr.Server.Domain.Modules.UserNotifications; + +namespace Coderr.Server.App.Core.Notifications +{ + /// + /// Repository for managing notification settings + /// + public interface INotificationsRepository + { + /// + /// Create settings + /// + /// settings + /// task + /// notificationSettings + Task CreateAsync(UserNotificationSettings notificationSettings); + + /// + /// Get application settings for all users. + /// + /// applicationId + /// Default setting will be returned for users that do not have any application specific. + /// applicationId + [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] + [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")] + Task> GetAllAsync(int applicationId); + + /// + /// Get settings + /// + /// account that want to modify it's settings + /// Application to modify settings for (0 = default settings) + /// settings if found; otherwise null. + /// accountId; applicationId + Task TryGetAsync(int accountId, int applicationId); + + /// + /// Update notifications + /// + /// settings + /// task + /// notificationSettings + Task UpdateAsync(UserNotificationSettings notificationSettings); + + Task Save(BrowserSubscription message); + Task DeleteBrowserSubscription(int accountId, string endpoint); + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs b/src/Server/Coderr.Server.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs new file mode 100644 index 00000000..4b70a03a --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs @@ -0,0 +1,111 @@ +using System.Data; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Abstractions.Reports; +using Griffin.ApplicationServices; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.App.Core.Reports.Jobs +{ + /// + /// Delete oldest reports for incidents with report count cap. + /// + /// + /// + /// You can configure the amount of reports per incident in the admin area. + /// + /// + [ContainerService(RegisterAsSelf = true)] + public class DeleteReportsBelowReportLimit : IBackgroundJob + { + private readonly ILog _logger = LogManager.GetLogger(typeof(DeleteReportsBelowReportLimit)); + private readonly IDbConnection _connection; + private readonly IConfiguration _reportConfig; + + /// + /// Creates a new instance of . + /// + /// Used for SQL queries + public DeleteReportsBelowReportLimit(IDbConnection connection, IConfiguration reportConfig) + { + _connection = connection; + _reportConfig = reportConfig; + } + + /// + /// Number of reports which can be stored per incident. + /// + public int MaxReportsPerIncident => _reportConfig?.Value?.MaxReportsPerIncident ?? 25; + + /// + public void Execute() + { + if (HostConfig.Instance.IsDemo) + return; + + using (var cmd = _connection.CreateCommand()) + { + var sql = $@"CREATE TABLE #Incidents (Id int NOT NULL PRIMARY KEY, NumberOfItems int) + INSERT #Incidents (Id, NumberOfItems) + SELECT TOP(100) IncidentId, Count(Id) - @max + FROM ErrorReports WITH (READUNCOMMITTED) + GROUP BY IncidentId + HAVING Count(Id) > @max + ORDER BY count(Id) DESC + + CREATE TABLE #ReportsToDelete (Id int not null primary key) + declare @counter int = 0; + + DECLARE IncidentCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY + FOR SELECT Id, NumberOfItems FROM #Incidents + DECLARE @IncidentId int + DECLARE @NumberOfItems int + OPEN IncidentCursor + FETCH NEXT FROM IncidentCursor INTO @IncidentId, @NumberOfItems + WHILE @@FETCH_STATUS = 0 + BEGIN + INSERT INTO #ReportsToDelete (Id) + SELECT TOP(@NumberOfItems) Id + FROM ErrorReports WITH (READUNCOMMITTED) + WHERE IncidentId = @IncidentId + ORDER BY Id asc + FETCH NEXT FROM IncidentCursor INTO @IncidentId, @NumberOfItems + END + CLOSE IncidentCursor + DEALLOCATE IncidentCursor + DROP TABLE #Incidents + + DECLARE ItemsToDeleteCursor CURSOR LOCAL FORWARD_ONLY READ_ONLY + FOR SELECT Id FROM #ReportsToDelete + + DECLARE @IdToDelete int + OPEN ItemsToDeleteCursor + FETCH NEXT FROM ItemsToDeleteCursor INTO @IdToDelete + + WHILE @@FETCH_STATUS = 0 + BEGIN + set @counter = @counter + 1 + DELETE FROM ErrorReports WHERE Id = @IdToDelete + FETCH NEXT FROM ItemsToDeleteCursor INTO @IdToDelete + END + + CLOSE ItemsToDeleteCursor + DEALLOCATE ItemsToDeleteCursor + DROP TABLE #ReportsToDelete + select @counter;"; + + cmd.CommandText = sql; + cmd.CommandTimeout = 10; + cmd.AddParameter("max", MaxReportsPerIncident); + var rows = (int)cmd.ExecuteScalar(); + if (rows > 0) + { + _logger.Debug("Deleted the oldest " + rows + " reports."); + } + + } + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Reports/PagedReports.cs b/src/Server/Coderr.Server.App/Core/Reports/PagedReports.cs similarity index 82% rename from src/Server/OneTrueError.App/Core/Reports/PagedReports.cs rename to src/Server/Coderr.Server.App/Core/Reports/PagedReports.cs index 2aff8fd7..d0255847 100644 --- a/src/Server/OneTrueError.App/Core/Reports/PagedReports.cs +++ b/src/Server/Coderr.Server.App/Core/Reports/PagedReports.cs @@ -1,22 +1,22 @@ -using System.Collections.Generic; -using OneTrueError.Api.Core.Reports; - -namespace OneTrueError.App.Core.Reports -{ - /// - /// Repository result - /// - /// - public class PagedReports - { - /// - /// Reports - /// - public IReadOnlyList Reports { get; set; } - - /// - /// Total count (the report collection can be paged) - /// - public int TotalCount { get; set; } - } +using System.Collections.Generic; +using Coderr.Server.Api.Core.Reports; + +namespace Coderr.Server.App.Core.Reports +{ + /// + /// Repository result + /// + /// + public class PagedReports + { + /// + /// Reports + /// + public IReadOnlyList Reports { get; set; } + + /// + /// Total count (the report collection can be paged) + /// + public int TotalCount { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Reports/Queries/GetReportHandler.cs b/src/Server/Coderr.Server.App/Core/Reports/Queries/GetReportHandler.cs similarity index 84% rename from src/Server/OneTrueError.App/Core/Reports/Queries/GetReportHandler.cs rename to src/Server/Coderr.Server.App/Core/Reports/Queries/GetReportHandler.cs index ae48c6b7..f8385a98 100644 --- a/src/Server/OneTrueError.App/Core/Reports/Queries/GetReportHandler.cs +++ b/src/Server/Coderr.Server.App/Core/Reports/Queries/GetReportHandler.cs @@ -1,90 +1,94 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Reports; -using OneTrueError.Api.Core.Reports.Queries; - -namespace OneTrueError.App.Core.Reports.Queries -{ - /// - /// Get report. - /// - [Component] - public class GetReportHandler : IQueryHandler - { - private readonly IReportsRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repository - /// repository - public GetReportHandler(IReportsRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// Method used to execute the query - /// Query to execute. - /// Task which will contain the result once completed. - public async Task ExecuteAsync(GetReport query) - { - var report = await _repository.GetAsync(query.ReportId); - var collections = ( - from x in report.ContextCollections - where x.Properties.Count > 0 - let properties = x.Properties.Select(y => new KeyValuePair(y.Key, y.Value)) - select new GetReportResultContextCollection(x.Name, properties.ToArray()) - ).ToList(); - - //TODO: Fix feedback - //var feedbackQuery = new GetReportFeedback(query.ReportId, query.);//TODO: Fix customerId - //var feedback = await _queryBus.QueryAsync(feedbackQuery); - //if (feedback != null) - //{ - // collections.Add(new GetReportResultContextCollection("UserFeedback", new[] - // { - // new KeyValuePair("EmailAddress", feedback.EmailAddress), - // new KeyValuePair("Description", feedback.Description) - // }) - // ); - //} - - return new GetReportResult - { - ContextCollections = collections.ToArray(), - CreatedAtUtc = report.CreatedAtUtc, - ErrorId = report.ReportId, - Exception = ConvertException(report.Exception), - Id = report.Id.ToString(), - IncidentId = report.IncidentId.ToString(), - Message = report.Exception.Message, - StackTrace = report.Exception.StackTrace - //UserFeedback = feedback != null ? feedback.Description : "", - //EmailAddress = feedback != null ? feedback.EmailAddress : "" - // ReportHashCode = report. - }; - } - - private GetReportException ConvertException(ReportExeptionDTO exception) - { - var ex = new GetReportException - { - AssemblyName = exception.AssemblyName, - BaseClasses = exception.BaseClasses, - FullName = exception.FullName, - Everything = exception.Everything, - Name = exception.Name, - Namespace = exception.Namespace, - Message = exception.Message, - StackTrace = exception.StackTrace - }; - if (exception.InnerException != null) - ex.InnerException = ConvertException(exception.InnerException); - return ex; - } - } +using System; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Reports.Queries; +using Coderr.Server.Domain.Core.ErrorReports; +using DotNetCqs; +using log4net; + + +namespace Coderr.Server.App.Core.Reports.Queries +{ + /// + /// Get report. + /// + public class GetReportHandler : IQueryHandler + { + private readonly IReportsRepository _repository; + private ILog _logger = LogManager.GetLogger(typeof(GetReportHandler)); + + /// + /// Creates a new instance of . + /// + /// repository + /// repository + public GetReportHandler(IReportsRepository repository) + { + if (repository == null) throw new ArgumentNullException("repository"); + _repository = repository; + } + + /// Method used to execute the query + /// Query to execute. + /// Task which will contain the result once completed. + public async Task HandleAsync(IMessageContext context, GetReport query) + { + _logger.Debug("Getting0.."); + var report = await _repository.GetAsync(query.ReportId); + _logger.Debug("Getting5.."); + var collections = ( + from x in report.ContextCollections + where x.Properties.Count > 0 + let properties = x.Properties.Select(y => new KeyValuePair(y.Key, y.Value)) + select new GetReportResultContextCollection(x.Name, properties.ToArray()) + ).ToList(); + + _logger.Debug("Getting6.."); + //TODO: Fix feedback + //var feedbackQuery = new GetReportFeedback(query.ReportId, query.);//TODO: Fix customerId + //var feedback = await _queryBus.QueryAsync(feedbackQuery); + //if (feedback != null) + //{ + // collections.Add(new GetReportResultContextCollection("UserFeedback", new[] + // { + // new KeyValuePair("EmailAddress", feedback.EmailAddress), + // new KeyValuePair("Description", feedback.Description) + // }) + // ); + //} + + return new GetReportResult + { + ContextCollections = collections.ToArray(), + CreatedAtUtc = report.CreatedAtUtc, + ErrorId = report.ClientReportId, + Exception = ConvertException(report.Exception), + Id = report.Id.ToString(), + IncidentId = report.IncidentId.ToString(), + Message = report.Exception.Message, + StackTrace = report.Exception.StackTrace + //UserFeedback = feedback != null ? feedback.Description : "", + //EmailAddress = feedback != null ? feedback.EmailAddress : "" + // ReportHashCode = report. + }; + } + + private GetReportException ConvertException(ErrorReportException exception) + { + var ex = new GetReportException + { + AssemblyName = exception.AssemblyName, + BaseClasses = exception.BaseClasses, + FullName = exception.FullName, + Everything = exception.Everything, + Name = exception.Name, + Namespace = exception.Namespace, + Message = exception.Message, + StackTrace = exception.StackTrace + }; + if (exception.InnerException != null) + ex.InnerException = ConvertException(exception.InnerException); + return ex; + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Support/SendSupportRequestHandler.cs b/src/Server/Coderr.Server.App/Core/Support/SendSupportRequestHandler.cs new file mode 100644 index 00000000..6811fcf9 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Support/SendSupportRequestHandler.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Accounts.Queries; +using Coderr.Server.Api.Core.Messaging; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Api.Core.Support; +using Coderr.Server.Infrastructure.Configuration; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Support +{ + /// + /// Sends a support request to the Coderr Team. + /// + /// + /// + /// You must have bought commercial support or registered to get 30 days of free support. + /// + /// + public class SendSupportRequestHandler : IMessageHandler + { + private ConfigurationStore _configStore; + + public SendSupportRequestHandler(ConfigurationStore configStore) + { + _configStore = configStore; + } + + /// + public async Task HandleAsync(IMessageContext context, SendSupportRequest command) + { + var baseConfig = _configStore.Load(); + var errorConfig = _configStore.Load(); + + string email = null; + var claim = context.Principal.FindFirst(ClaimTypes.Email); + if (claim != null) + email = claim.Value; + else + { + var user = await context.QueryAsync(new GetAccountById(context.Principal.GetAccountId())); + email = user.Email; + } + + string installationId = "Coderr"; + if (string.IsNullOrEmpty(email)) + email = baseConfig.SupportEmail; + + if (errorConfig != null) + { + if (!string.IsNullOrEmpty(errorConfig.ContactEmail) && string.IsNullOrEmpty(errorConfig.ContactEmail)) + email = errorConfig.ContactEmail; + + installationId = errorConfig.InstallationId; + } + + // A support contact have been specified. + // Thus this is a OnPremise/community server installation + if (!ServerConfig.Instance.IsLive) + { + var msg = new EmailMessage(email) + { + Subject = command.Subject, + ReplyTo = new EmailAddress(email), + TextBody = $"Request from: {email}\r\n\r\n{command.Message}" + }; + var cmd = new SendEmail(msg); + await context.SendAsync(cmd); + return; + } + + var items = new List>(); + if (installationId != null) + items.Add(new KeyValuePair("InstallationId", installationId)); + items.Add(new KeyValuePair("ContactEmail", email)); + items.Add(new KeyValuePair("Subject", command.Subject)); + items.Add(new KeyValuePair("Message", command.Message)); + + //To know which page the user had trouble with + items.Add(new KeyValuePair("PageUrl", command.Url)); + + var content = new FormUrlEncodedContent(items); + var client = new HttpClient + { + Timeout = TimeSpan.FromSeconds(5) + }; + await client.PostAsync("https://coderr.io/support/request", content); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Users/EventHandlers/CreateOnNewAccount.cs b/src/Server/Coderr.Server.App/Core/Users/EventHandlers/CreateOnNewAccount.cs new file mode 100644 index 00000000..ae384855 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Users/EventHandlers/CreateOnNewAccount.cs @@ -0,0 +1,33 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Accounts.Events; +using Coderr.Server.Domain.Core.User; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Users.EventHandlers +{ + /// + /// Responsible of creating an user entity when a new account is created. + /// + internal class CreateOnNewAccount : IMessageHandler + { + private readonly IUserRepository _userRepository; + + public CreateOnNewAccount(IUserRepository userRepository) + { + _userRepository = userRepository; + } + + public async Task HandleAsync(IMessageContext context, AccountActivated e) + { + var user = await _userRepository.FindByEmailAsync(e.EmailAddress); + if (user != null) + return; + + await _userRepository.CreateAsync(new User(e.AccountId, e.UserName) + { + EmailAddress = e.EmailAddress + }); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Users/WebApi/GetUserSettingsHandler.cs b/src/Server/Coderr.Server.App/Core/Users/WebApi/GetUserSettingsHandler.cs new file mode 100644 index 00000000..fde101bc --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Users/WebApi/GetUserSettingsHandler.cs @@ -0,0 +1,47 @@ +using System.Threading.Tasks; +using Coderr.Server.Api; +using Coderr.Server.Api.Core.Users; +using Coderr.Server.Api.Core.Users.Queries; +using Coderr.Server.App.Core.Notifications; +using Coderr.Server.Domain.Core.User; +using Coderr.Server.Domain.Modules.UserNotifications; +using DotNetCqs; +using NotificationState = Coderr.Server.Api.Core.Users.NotificationState; + +namespace Coderr.Server.App.Core.Users.WebApi +{ + internal class GetUserSettingsHandler : IQueryHandler + { + private readonly INotificationsRepository _repository; + private readonly IUserRepository _userRepository; + + public GetUserSettingsHandler(INotificationsRepository repository, IUserRepository userRepository) + { + _repository = repository; + _userRepository = userRepository; + } + + public async Task HandleAsync(IMessageContext context, GetUserSettings query) + { + var settings = await _repository.TryGetAsync(query.UserId, query.ApplicationId) + ?? new UserNotificationSettings(query.UserId, query.ApplicationId); + var user = await _userRepository.GetUserAsync(query.UserId); + return new GetUserSettingsResult + { + FirstName = user.FirstName, + LastName = user.LastName, + MobileNumber = user.MobileNumber, + EmailAddress = user.EmailAddress, + Notifications = new NotificationSettings + { + NotifyOnReOpenedIncident = settings.ReopenedIncident.ConvertEnum(), + NotifyOnUserFeedback = settings.UserFeedback.ConvertEnum(), + NotifyOnPeaks = settings.ApplicationSpike.ConvertEnum(), + NotifyOnNewIncidents = settings.NewIncident.ConvertEnum(), + NotifyOnCriticalIncidents = settings.CriticalIncident.ConvertEnum(), + NotifyOnImportantIncidents = settings.ImportantIncident.ConvertEnum() + } + }; + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Users/WebApi/UpdateNotificationsHandler.cs b/src/Server/Coderr.Server.App/Core/Users/WebApi/UpdateNotificationsHandler.cs similarity index 76% rename from src/Server/OneTrueError.App/Core/Users/WebApi/UpdateNotificationsHandler.cs rename to src/Server/Coderr.Server.App/Core/Users/WebApi/UpdateNotificationsHandler.cs index c2cbef88..386da58f 100644 --- a/src/Server/OneTrueError.App/Core/Users/WebApi/UpdateNotificationsHandler.cs +++ b/src/Server/Coderr.Server.App/Core/Users/WebApi/UpdateNotificationsHandler.cs @@ -1,54 +1,55 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core; -using OneTrueError.Api.Core.Users.Commands; -using OneTrueError.App.Core.Notifications; - -namespace OneTrueError.App.Core.Users.WebApi -{ - /// - /// Handler for . - /// - [Component] - public class UpdateNotificationsHandler : ICommandHandler - { - private readonly INotificationsRepository _notificationsRepository; - - /// - /// Creates a new instance of . - /// - /// notifications repository - /// notificationsRepository - public UpdateNotificationsHandler(INotificationsRepository notificationsRepository) - { - if (notificationsRepository == null) throw new ArgumentNullException("notificationsRepository"); - _notificationsRepository = notificationsRepository; - } - - /// - /// Execute a command asynchronously. - /// - /// Command to execute. - /// - /// Task which will be completed once the command has been executed. - /// - public async Task ExecuteAsync(UpdateNotifications command) - { - var settings = await _notificationsRepository.TryGetAsync(command.UserId, command.ApplicationId); - if (settings == null) - { - settings = new UserNotificationSettings(command.UserId, command.ApplicationId); - await _notificationsRepository.CreateAsync(settings); - } - - settings.ApplicationSpike = command.NotifyOnPeaks.ConvertEnum(); - settings.NewIncident = command.NotifyOnNewIncidents.ConvertEnum(); - settings.NewReport = command.NotifyOnNewReport.ConvertEnum(); - settings.ReopenedIncident = command.NotifyOnReOpenedIncident.ConvertEnum(); - settings.UserFeedback = command.NotifyOnUserFeedback.ConvertEnum(); - await _notificationsRepository.UpdateAsync(settings); - } - } +using System; +using System.Threading.Tasks; +using Coderr.Server.Api; +using Coderr.Server.Api.Core.Users.Commands; +using Coderr.Server.App.Core.Notifications; +using Coderr.Server.Domain.Modules.UserNotifications; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Users.WebApi +{ + /// + /// Handler for . + /// + public class UpdateNotificationsHandler : IMessageHandler + { + private readonly INotificationsRepository _notificationsRepository; + + /// + /// Creates a new instance of . + /// + /// notifications repository + /// notificationsRepository + public UpdateNotificationsHandler(INotificationsRepository notificationsRepository) + { + if (notificationsRepository == null) throw new ArgumentNullException("notificationsRepository"); + _notificationsRepository = notificationsRepository; + } + + /// + /// Execute a command asynchronously. + /// + /// Command to execute. + /// + /// Task which will be completed once the command has been executed. + /// + public async Task HandleAsync(IMessageContext context, UpdateNotifications command) + { + var settings = await _notificationsRepository.TryGetAsync(command.UserId, command.ApplicationId); + if (settings == null) + { + settings = new UserNotificationSettings(command.UserId, command.ApplicationId); + await _notificationsRepository.CreateAsync(settings); + } + + settings.ApplicationSpike = command.NotifyOnPeaks.ConvertEnum(); + settings.NewIncident = command.NotifyOnNewIncidents.ConvertEnum(); + settings.CriticalIncident = command.NotifyOnCriticalIncidents.ConvertEnum(); + settings.ImportantIncident = command.NotifyOnImportantIncidents.ConvertEnum(); + settings.ReopenedIncident = command.NotifyOnReOpenedIncident.ConvertEnum(); + settings.UserFeedback = command.NotifyOnUserFeedback.ConvertEnum(); + await _notificationsRepository.UpdateAsync(settings); + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Core/Users/WebApi/UpdatePersonalSettingsHandler.cs b/src/Server/Coderr.Server.App/Core/Users/WebApi/UpdatePersonalSettingsHandler.cs new file mode 100644 index 00000000..bc256370 --- /dev/null +++ b/src/Server/Coderr.Server.App/Core/Users/WebApi/UpdatePersonalSettingsHandler.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Users.Commands; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.Domain.Core.User; +using DotNetCqs; + + +namespace Coderr.Server.App.Core.Users.WebApi +{ + /// + /// Handler for . + /// + public class UpdatePersonalSettingsHandler : IMessageHandler + { + private readonly IUserRepository _userRepository; + private readonly IAccountRepository _accountRepository; + + /// + /// Creates a new instance of . + /// + /// repos + /// Used to change email + /// userRepository + public UpdatePersonalSettingsHandler(IUserRepository userRepository, IAccountRepository accountRepository) + { + _userRepository = userRepository ?? throw new ArgumentNullException(nameof(userRepository)); + _accountRepository = accountRepository ?? throw new ArgumentNullException(nameof(accountRepository)); + } + + /// + /// Execute a command asynchronously. + /// + /// Command to execute. + /// + /// Task which will be completed once the command has been executed. + /// + public async Task HandleAsync(IMessageContext context, UpdatePersonalSettings command) + { + if (command == null) throw new ArgumentNullException(nameof(command)); + + var user = await _userRepository.GetUserAsync(command.UserId); + user.FirstName = command.FirstName; + user.LastName = command.LastName; + user.MobileNumber = command.MobileNumber; + + if (command.EmailAddress != null) + user.EmailAddress = command.EmailAddress; + + await _userRepository.UpdateAsync(user); + + if (command.EmailAddress == null) + return; + + var account = await _accountRepository.GetByIdAsync(user.AccountId); + account.SetVerifiedEmail(command.EmailAddress); + await _accountRepository.UpdateAsync(account); + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/CustomDictionary.xml b/src/Server/Coderr.Server.App/CustomDictionary.xml similarity index 100% rename from src/Server/OneTrueError.App/CustomDictionary.xml rename to src/Server/Coderr.Server.App/CustomDictionary.xml diff --git a/src/Server/Coderr.Server.App/GlobalSuppressions.cs b/src/Server/Coderr.Server.App/GlobalSuppressions.cs new file mode 100644 index 00000000..2860fa02 --- /dev/null +++ b/src/Server/Coderr.Server.App/GlobalSuppressions.cs @@ -0,0 +1,254 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("Coderr.Server.App.Tests")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "*")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Modules.Tagging.Handlers.GetTagsForApplicationHandler.#.ctor(Coderr.App.Modules.Tagging.ITagsRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Applications.QueryHandlers.GetApplicationTeamHandler.#.ctor(Coderr.App.Core.Applications.IApplicationRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Accounts.Account.#Id")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Feedback.EventSubscribers.AttachFeedbackToIncident.#.ctor(Coderr.App.Core.Feedback.IFeedbackRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Applications.CommandHandlers.CreateApplicationHandler.#.ctor(Coderr.App.Core.Applications.IApplicationRepository,Coderr.App.Core.Users.IUserRepository,DotNetCqs.IMessageBus)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Applications.EventHandlers.CreateDefaultAppOnAccountActivated.#.ctor(DotNetCqs.IMessageBus)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Users.EventHandlers.CreateOnNewAccount.#.ctor(Coderr.App.Core.Users.IUserRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Modules.Messaging.Templating.DateFormatter.#FormatAgo(System.DateTime)")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Users.WebApi.GetUserSettingsHandler.#.ctor(Coderr.App.Core.Notifications.INotificationsRepository,Coderr.App.Core.Users.IUserRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Incidents.Incident.#ApplicationId")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Incidents.Incident.#CreatedAtUtc")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Feedback.InvalidErrorReport.#Id")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Reports.Invalid.InvalidErrorReport.#Id")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Invitations.CommandHandlers.InviteUserHandler.#.ctor(Coderr.App.Core.Invitations.Data.IInvitationRepository,DotNetCqs.IMessageBus,Coderr.App.Core.Users.IUserRepository,Coderr.App.Core.Applications.IApplicationRepository,DotNetCqs.IMessageBus)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Modules.Triggers.Domain.Actions.SendEmailTask.#.ctor(DotNetCqs.IMessageBus)")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Applications.EventHandlers.UpdateTeamOnInvitationAccepted.#.ctor(Coderr.App.Core.Applications.IApplicationRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Configuration.ErrorTracking")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Configuration.Messaging")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Accounts")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Accounts.CommandHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Accounts.Queries")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Applications")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Applications.QueryHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Feedback")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Feedback.EventSubscribers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Incidents")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Incidents.Commands")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Invitations")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Invitations.Data")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Invitations.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Notifications")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Notifications.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Notifications.Tasks")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Reports")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Reports.Invalid")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Reports.Queries")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Users")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Core.Users.WebApi")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Geolocation")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Geolocation.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Messaging")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Messaging.Commands")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.ReportSpikes")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Similarities.Domain.Adapters.Normalizers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Similarities.Domain.Adapters.OperatingSystems")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Similarities.Domain.Adapters.Runner")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Similarities.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Tagging.Domain")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Tagging.Handlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Triggers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Triggers.Commands")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Triggers.Domain.Actions.Tools")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Triggers.Domain.Rules")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Triggers.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Triggers.Queries")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re", Scope = "member", + Target = "Coderr.App.Modules.Triggers.Domain.Trigger.#RunForReOpenedIncidents")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re", Scope = "member", + Target = "Coderr.App.Core.Notifications.UserNotificationSettings.#ReOpenedIncident")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ROLE", Scope = "member", + Target = "Coderr.App.Core.Applications.Application.#ROLE_ADMIN")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ROLE", Scope = "member", + Target = "Coderr.App.Core.Applications.Application.#ROLE_MEMBER")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SEQUENCE", + Scope = "member", Target = "Coderr.App.Core.Accounts.Account.#SEQUENCE")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Api", + Scope = "namespace", Target = "Coderr.App.Core.Users.WebApi")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Username", + Scope = "member", + Target = "Coderr.App.Core.Accounts.IAccountRepository.#FindByUsernameAsync(System.String)")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterCondition", + Scope = "type", Target = "Coderr.App.Modules.Triggers.Domain.FilterCondition")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterCondition", + Scope = "member", + Target = + "Coderr.App.Modules.Triggers.Queries.DomainToDtoConverters.#ConvertFilterCondition(Coderr.App.Modules.Triggers.Domain.FilterCondition)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "Coderr.App.Core.Invitations.CommandHandlers.AcceptInvitationHandler.#.ctor(Coderr.App.Core.Invitations.Data.IInvitationRepository,Coderr.App.Core.Accounts.IAccountRepository,DotNetCqs.IMessageBus)" + )] +[assembly: + SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Scope = "member", + Target = + "Coderr.App.Modules.Similarities.Domain.Adapters.OperatingSystemAdapter.#Adapt(Coderr.App.Modules.Similarities.Domain.Adapters.Runner.ValueAdapterContext,System.Object)" + )] +[assembly: SuppressMessage("Microsoft.Design", "CA1014:MarkAssembliesWithClsCompliant")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "Coderr.App.Modules.Tagging")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Feedback.FeedbackEntity.#CreatedAtUtc")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Feedback.FeedbackEntity.#Description")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Feedback.FeedbackEntity.#EmailAddress")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Feedback.FeedbackEntity.#ErrorId")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Incidents.Incident.#Id")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "Coderr.App.Core.Invitations.Invitation.#Id")] diff --git a/src/Server/Coderr.Server.App/GlobalSuppressions.cs.orig b/src/Server/Coderr.Server.App/GlobalSuppressions.cs.orig new file mode 100644 index 00000000..50fbefad --- /dev/null +++ b/src/Server/Coderr.Server.App/GlobalSuppressions.cs.orig @@ -0,0 +1,258 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +[assembly:InternalsVisibleTo("codeRR.Server.App.Tests")] +[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "*")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = +<<<<<<< HEAD + "codeRR.Server.App.Modules.Tagging.Handlers.GetTagsForIncidentHandler.#.ctor(codeRR.Server.App.Modules.Tagging.ITagsRepository)" +======= + "codeRR.App.Modules.Tagging.Handlers.GetTagsForApplicationHandler.#.ctor(codeRR.App.Modules.Tagging.ITagsRepository)" +>>>>>>> upstream/master + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Applications.QueryHandlers.GetApplicationTeamHandler.#.ctor(codeRR.Server.App.Core.Applications.IApplicationRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Accounts.Account.#Id")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Feedback.EventSubscribers.AttachFeedbackToIncident.#.ctor(codeRR.Server.App.Core.Feedback.IFeedbackRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Applications.CommandHandlers.CreateApplicationHandler.#.ctor(codeRR.Server.App.Core.Applications.IApplicationRepository,codeRR.Server.App.Core.Users.IUserRepository,DotNetCqs.IEventBus)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Applications.EventHandlers.CreateDefaultAppOnAccountActivated.#.ctor(DotNetCqs.ICommandBus)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Users.EventHandlers.CreateOnNewAccount.#.ctor(codeRR.Server.App.Core.Users.IUserRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Modules.Messaging.Templating.DateFormatter.#FormatAgo(System.DateTime)")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Users.WebApi.GetUserSettingsHandler.#.ctor(codeRR.Server.App.Core.Notifications.INotificationsRepository,codeRR.Server.App.Core.Users.IUserRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Incidents.Incident.#ApplicationId")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Incidents.Incident.#CreatedAtUtc")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Feedback.InvalidErrorReport.#Id")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Reports.Invalid.InvalidErrorReport.#Id")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Invitations.CommandHandlers.InviteUserHandler.#.ctor(codeRR.Server.App.Core.Invitations.Data.IInvitationRepository,DotNetCqs.IEventBus,codeRR.Server.App.Core.Users.IUserRepository,codeRR.Server.App.Core.Applications.IApplicationRepository,DotNetCqs.ICommandBus)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Modules.Triggers.Domain.Actions.SendEmailTask.#.ctor(DotNetCqs.ICommandBus)")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Applications.EventHandlers.UpdateTeamOnInvitationAccepted.#.ctor(codeRR.Server.App.Core.Applications.IApplicationRepository)" + )] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Configuration.ErrorTracking")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Configuration.Messaging")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Accounts")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Accounts.CommandHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Accounts.Queries")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Applications")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Applications.QueryHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Feedback")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Feedback.EventSubscribers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Incidents")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Incidents.Commands")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Invitations")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Invitations.Data")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Invitations.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Notifications")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Notifications.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Notifications.Tasks")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Reports")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Reports.Invalid")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Reports.Queries")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Users")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Core.Users.WebApi")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Geolocation")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Geolocation.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Messaging")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Messaging.Commands")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.ReportSpikes")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Similarities.Domain.Adapters.Normalizers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Similarities.Domain.Adapters.OperatingSystems")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Similarities.Domain.Adapters.Runner")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Similarities.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Tagging.Domain")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Tagging.Handlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Triggers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Triggers.Commands")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Triggers.Domain.Actions.Tools")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Triggers.Domain.Rules")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Triggers.EventHandlers")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Triggers.Queries")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re", Scope = "member", + Target = "codeRR.Server.App.Modules.Triggers.Domain.Trigger.#RunForReOpenedIncidents")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re", Scope = "member", + Target = "codeRR.Server.App.Core.Notifications.UserNotificationSettings.#ReOpenedIncident")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ROLE", Scope = "member", + Target = "codeRR.Server.App.Core.Applications.Application.#ROLE_ADMIN")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ROLE", Scope = "member", + Target = "codeRR.Server.App.Core.Applications.Application.#ROLE_MEMBER")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SEQUENCE", + Scope = "member", Target = "codeRR.Server.App.Core.Accounts.Account.#SEQUENCE")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Api", + Scope = "namespace", Target = "codeRR.Server.App.Core.Users.WebApi")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Username", + Scope = "member", + Target = "codeRR.Server.App.Core.Accounts.IAccountRepository.#FindByUsernameAsync(System.String)")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterCondition", + Scope = "type", Target = "codeRR.Server.App.Modules.Triggers.Domain.FilterCondition")] +[assembly: + SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterCondition", + Scope = "member", + Target = + "codeRR.Server.App.Modules.Triggers.Queries.DomainToDtoConverters.#ConvertFilterCondition(codeRR.Server.App.Modules.Triggers.Domain.FilterCondition)" + )] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = + "codeRR.Server.App.Core.Invitations.CommandHandlers.AcceptInvitationHandler.#.ctor(codeRR.Server.App.Core.Invitations.Data.IInvitationRepository,codeRR.Server.App.Core.Accounts.IAccountRepository,DotNetCqs.IEventBus)" + )] +[assembly: + SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Scope = "member", + Target = + "codeRR.Server.App.Modules.Similarities.Domain.Adapters.OperatingSystemAdapter.#Adapt(codeRR.Server.App.Modules.Similarities.Domain.Adapters.Runner.ValueAdapterContext,System.Object)" + )] +[assembly: SuppressMessage("Microsoft.Design", "CA1014:MarkAssembliesWithClsCompliant")] +[assembly: + SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", + Target = "codeRR.Server.App.Modules.Tagging")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Feedback.FeedbackEntity.#CreatedAtUtc")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Feedback.FeedbackEntity.#Description")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Feedback.FeedbackEntity.#EmailAddress")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Feedback.FeedbackEntity.#ErrorId")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Incidents.Incident.#Id")] +[assembly: + SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", + Target = "codeRR.Server.App.Core.Invitations.Invitation.#Id")] diff --git a/src/Server/Coderr.Server.App/Modules/History/Events/IncidentAssignedHandler.cs b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentAssignedHandler.cs new file mode 100644 index 00000000..d1695ee4 --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentAssignedHandler.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Events; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Modules.History; +using DotNetCqs; + +namespace Coderr.Server.App.Modules.History.Events +{ + public class IncidentAssignedHandler : IMessageHandler + { + private readonly IHistoryRepository _repository; + + public IncidentAssignedHandler(IHistoryRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, IncidentAssigned message) + { + //var entries = await _repository.GetByIncidentId(message.IncidentId); + + //// Include the version to make it easier to fetch information + //// that should be shown (to give the user a hint on version difference between created and assigned states) + //var version = entries.Where(x => x.ApplicationVersion != null).Select(x => x.ApplicationVersion) + // .LastOrDefault(); + + var entry = new HistoryEntry(message.IncidentId, message.AssignedById, IncidentState.Active); + await _repository.CreateAsync(entry); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/History/Events/IncidentClosedHandler.cs b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentClosedHandler.cs new file mode 100644 index 00000000..d0a498ec --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentClosedHandler.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Events; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Modules.History; +using DotNetCqs; + +namespace Coderr.Server.App.Modules.History.Events +{ + internal class IncidentClosedHandler : IMessageHandler + { + private readonly IHistoryRepository _repository; + + public IncidentClosedHandler(IHistoryRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, IncidentClosed message) + { + var entry = new HistoryEntry(message.IncidentId, message.ClosedById, IncidentState.Closed) + { + ApplicationVersion = message.ApplicationVersion + }; + await _repository.CreateAsync(entry); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/History/Events/IncidentCreatedHandler.cs b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentCreatedHandler.cs new file mode 100644 index 00000000..3f677e78 --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentCreatedHandler.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Core.Incidents.Events; +using Coderr.Server.Domain.Modules.History; +using Coderr.Server.Infrastructure.Security; +using DotNetCqs; + +namespace Coderr.Server.App.Modules.History.Events +{ + internal class IncidentCreatedHandler : IMessageHandler + { + private readonly IHistoryRepository _historyRepository; + + public IncidentCreatedHandler(IHistoryRepository historyRepository) + { + _historyRepository = historyRepository; + } + + public async Task HandleAsync(IMessageContext context, IncidentCreated message) + { + var entry = new HistoryEntry(message.IncidentId, null, IncidentState.New) + { + ApplicationVersion = message.ApplicationVersion + }; + await _historyRepository.CreateAsync(entry); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/History/Events/IncidentIgnoredHandler.cs b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentIgnoredHandler.cs new file mode 100644 index 00000000..dd4a8595 --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentIgnoredHandler.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Incidents.Events; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Modules.History; +using DotNetCqs; + +namespace Coderr.Server.App.Modules.History.Events +{ + public class IncidentIgnoredHandler : IMessageHandler + { + private readonly IHistoryRepository _repository; + + public IncidentIgnoredHandler(IHistoryRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, IncidentIgnored message) + { + var entry = new HistoryEntry(message.IncidentId, message.AccountId, IncidentState.Ignored); + await _repository.CreateAsync(entry); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/History/Events/IncidentReOpenedHandler.cs b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentReOpenedHandler.cs new file mode 100644 index 00000000..d14114c9 --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/History/Events/IncidentReOpenedHandler.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Domain.Core.Incidents; +using Coderr.Server.Domain.Core.Incidents.Events; +using Coderr.Server.Domain.Modules.History; +using DotNetCqs; + +namespace Coderr.Server.App.Modules.History.Events +{ + public class IncidentReOpenedHandler : IMessageHandler + { + private readonly IHistoryRepository _repository; + + public IncidentReOpenedHandler(IHistoryRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, IncidentReOpened message) + { + int? accountId; + + try + { + if (context.Principal.IsInRole(CoderrRoles.System)) + accountId = null; + else + accountId = context.Principal.GetAccountId(); + } + catch (Exception ex) + { + ex.Data["Principal"] = context.Principal.ToFriendlyString(); + throw; + } + + var e = new HistoryEntry(message.IncidentId, accountId, IncidentState.ReOpened) + { + ApplicationVersion = message.ApplicationVersion + }; + await _repository.CreateAsync(e); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/History/Queries/GetIncidentStateSummaryHandler.cs b/src/Server/Coderr.Server.App/Modules/History/Queries/GetIncidentStateSummaryHandler.cs new file mode 100644 index 00000000..47c5db40 --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/History/Queries/GetIncidentStateSummaryHandler.cs @@ -0,0 +1,58 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.History.Queries; +using Coderr.Server.Domain.Core.Incidents; +using DotNetCqs; +using Griffin.Data; + +namespace Coderr.Server.App.Modules.History.Queries +{ + internal class GetIncidentStateSummaryHandler : IMessageHandler + { + private readonly IAdoNetUnitOfWork _unitOfWork; + + public GetIncidentStateSummaryHandler(IAdoNetUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task HandleAsync(IMessageContext context, GetIncidentStateSummary message) + { + using (var cmd = _unitOfWork.CreateDbCommand()) + { + cmd.CommandText = @"select ih.State, count(*) as Count + from IncidentHistory ih + join IncidentVersions iv on (ih.IncidentId = iv.IncidentId) + join ApplicationVersions av on (av.Id = iv.VersionId) + WHERE ih.ApplicationVersion = @version + AND av.ApplicationId = @appId + group by ih.State + "; + + cmd.AddParameter("version", message.ApplicationVersion); + cmd.AddParameter("appId", message.ApplicationId); + + var entry = new GetIncidentStateSummaryResult(); + using (var reader = await cmd.ExecuteReaderAsync()) + { + while (await reader.ReadAsync()) + { + var state = (IncidentState) reader.GetInt32(0); + var count = reader.GetInt32(1); + switch (state) + { + case IncidentState.New: + entry.NewCount = count; + break; + case IncidentState.ReOpened: + entry.ReOpenedCount = count; + break; + case IncidentState.Closed: + entry.ClosedCount = count; + break; + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/Logs/Handlers/GetLogsHandler.cs b/src/Server/Coderr.Server.App/Modules/Logs/Handlers/GetLogsHandler.cs new file mode 100644 index 00000000..5ca4e9f4 --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/Logs/Handlers/GetLogsHandler.cs @@ -0,0 +1,35 @@ +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.Logs.Queries; +using Coderr.Server.Domain.Modules.Logs; +using DotNetCqs; + +namespace Coderr.Server.App.Modules.Logs.Handlers +{ + internal class GetLogsHandler : IQueryHandler + { + private readonly ILogsRepository _logsRepository; + + public GetLogsHandler(ILogsRepository logsRepository) + { + _logsRepository = logsRepository; + } + + public async Task HandleAsync(IMessageContext context, GetLogs query) + { + var entries = await _logsRepository.Get(query.IncidentId, query.ReportId); + return new GetLogsResult + { + Entries = entries + .Select(x => new GetLogsResultEntry + { + TimeStampUtc = x.TimeStampUtc, + Exception = x.Exception, + Level = (GetLogsResultEntryLevel) x.Level, + Message = x.Message + }) + .ToArray() + }; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/Logs/Handlers/HasLogsHandler.cs b/src/Server/Coderr.Server.App/Modules/Logs/Handlers/HasLogsHandler.cs new file mode 100644 index 00000000..87bf2f3d --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/Logs/Handlers/HasLogsHandler.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Coderr.Server.Api.Modules.Logs.Queries; +using Coderr.Server.Domain.Modules.Logs; +using DotNetCqs; + +namespace Coderr.Server.App.Modules.Logs.Handlers +{ + internal class HasLogsHandler : IQueryHandler + { + private readonly ILogsRepository _repository; + + public HasLogsHandler(ILogsRepository repository) + { + _repository = repository; + } + + public async Task HandleAsync(IMessageContext context, HasLogs query) + { + return new HasLogsReply { HasLogs = await _repository.Exists(query.IncidentId, query.ReportId) }; + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs similarity index 82% rename from src/Server/OneTrueError.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs index 584cf4a0..a6766257 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Commands/DotNetSmtpSettings.cs @@ -1,63 +1,64 @@ -using System.Collections.Generic; -using System.Globalization; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Modules.Messaging.Commands -{ - /// - /// Used to configure the SmtpClient which is part of .NET - /// - public sealed class DotNetSmtpSettings : IConfigurationSection - { - /// - /// Account name used to authenticate against the mail server - /// - public string AccountName { get; set; } - - /// - /// Password for . - /// - public string AccountPassword { get; set; } - - /// - /// Port number (25 or 587 depending on if SSL is used) - /// - public int PortNumber { get; set; } - - /// - /// Ip address or host name for the SMTP server - /// - public string SmtpHost { get; set; } - - /// - /// Use SSL when communicating with the SMTP server - /// - public bool UseSsl { get; set; } - - string IConfigurationSection.SectionName - { - get { return "SmtpSettings"; } - } - - IDictionary IConfigurationSection.ToDictionary() - { - return new Dictionary - { - {"AccountName", AccountName}, - {"AccountPassword", AccountPassword}, - {"SmtpHost", SmtpHost}, - {"PortNumber", PortNumber.ToString()}, - {"UseSSL", UseSsl.ToString(CultureInfo.InvariantCulture)} - }; - } - - void IConfigurationSection.Load(IDictionary items) - { - AccountName = items.GetString("AccountName"); - AccountPassword = items.GetString("AccountPassword", ""); - SmtpHost = items.GetString("SmtpHost"); - PortNumber = items.GetInteger("PortNumber"); - UseSsl = items.GetBoolean("UseSsl"); - } - } +using System.Collections.Generic; +using System.Globalization; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Infrastructure.Configuration; + +namespace Coderr.Server.App.Modules.Messaging.Commands +{ + /// + /// Used to configure the SmtpClient which is part of .NET + /// + public sealed class DotNetSmtpSettings : IConfigurationSection + { + /// + /// Account name used to authenticate against the mail server + /// + public string AccountName { get; set; } + + /// + /// Password for . + /// + public string AccountPassword { get; set; } + + /// + /// Port number (25 or 587 depending on if SSL is used) + /// + public int PortNumber { get; set; } + + /// + /// Ip address or host name for the SMTP server + /// + public string SmtpHost { get; set; } + + /// + /// Use SSL when communicating with the SMTP server + /// + public bool UseSsl { get; set; } + + string IConfigurationSection.SectionName + { + get { return "SmtpSettings"; } + } + + IDictionary IConfigurationSection.ToDictionary() + { + return new Dictionary + { + {"AccountName", AccountName}, + {"AccountPassword", AccountPassword}, + {"SmtpHost", SmtpHost}, + {"PortNumber", PortNumber.ToString()}, + {"UseSSL", UseSsl.ToString(CultureInfo.InvariantCulture)} + }; + } + + void IConfigurationSection.Load(IDictionary items) + { + AccountName = items.GetString("AccountName", ""); + AccountPassword = items.GetString("AccountPassword", ""); + SmtpHost = items.GetString("SmtpHost", ""); + PortNumber = items.GetInteger("PortNumber", null); + UseSsl = items.GetBoolean("UseSSL", false); + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/Messaging/Commands/SendEmailHandler.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Commands/SendEmailHandler.cs new file mode 100644 index 00000000..636a821e --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Commands/SendEmailHandler.cs @@ -0,0 +1,114 @@ +using System.IO; +using System.Net; +using System.Net.Mail; +using System.Net.Mime; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Api.Core.Accounts.Queries; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.Infrastructure.Configuration; +using Coderr.Server.Infrastructure.Net; +using DotNetCqs; + +using Markdig; + +namespace Coderr.Server.App.Modules.Messaging.Commands +{ + public class SendEmailHandler : IMessageHandler + { + private ConfigurationStore _configStore; + + public SendEmailHandler(ConfigurationStore configStore) + { + _configStore = configStore; + } + + public async Task HandleAsync(IMessageContext context, SendEmail command) + { + // Emails have been disabled. Typically just in LIVE. + if (!ServerConfig.Instance.UseSmtpHandler) + return; + + var client = CreateSmtpClient(); + if (client == null) + return; + + var baseConfig = _configStore.Load(); + + + var email = new MailMessage + { + From = new MailAddress(baseConfig.SupportEmail), + Subject = command.EmailMessage.Subject + }; + if (command.EmailMessage.ReplyTo != null) + { + var address = new MailAddress(command.EmailMessage.ReplyTo.Address,command.EmailMessage.ReplyTo.Name); + email.ReplyToList.Add(address); + } + + var markdownHtml = Markdown.ToHtml(command.EmailMessage.TextBody ?? ""); + + + if (string.IsNullOrEmpty(command.EmailMessage.HtmlBody) && markdownHtml == command.EmailMessage.TextBody) + { + email.Body = command.EmailMessage.TextBody; + email.IsBodyHtml = false; + await client.SendMailAsync(email); + return; + } + + if (string.IsNullOrEmpty(command.EmailMessage.HtmlBody)) + command.EmailMessage.HtmlBody = markdownHtml; + + var av = AlternateView.CreateAlternateViewFromString(command.EmailMessage.HtmlBody, null, + MediaTypeNames.Text.Html); + if (!string.IsNullOrEmpty(command.EmailMessage.TextBody)) + email.Body = command.EmailMessage.TextBody; + foreach (var resource in command.EmailMessage.Resources) + { + var contentType = new ContentType(MimeMapping.GetMimeType(Path.GetExtension(resource.Name))); + var ms = new MemoryStream(resource.Content, 0, resource.Content.Length, false); + var linkedResource = new LinkedResource(ms) + { + ContentId = resource.Name, + ContentType = contentType + }; + av.LinkedResources.Add(linkedResource); + } + + email.AlternateViews.Add(av); + + // Send one email per recipient to not share addresses. + foreach (var recipient in command.EmailMessage.Recipients) + { + email.To.Clear(); + if (int.TryParse(recipient.Address, out var accountId)) + { + var query = new GetAccountEmailById(accountId); + var emailAddress = await context.QueryAsync(query); + email.To.Add(new MailAddress(emailAddress, recipient.Name)); + } + else + email.To.Add(new MailAddress(recipient.Address, recipient.Name)); + + await client.SendMailAsync(email); + } + + } + + private SmtpClient CreateSmtpClient() + { + var config = _configStore.Load(); + if (string.IsNullOrEmpty(config.SmtpHost)) + return null; + + var client = new SmtpClient(config.SmtpHost, config.PortNumber); + if (!string.IsNullOrEmpty(config.AccountName)) + client.Credentials = new NetworkCredential(config.AccountName, config.AccountPassword); + client.EnableSsl = config.UseSsl; + return client; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/Messaging/Commands/SendTemplateEmailHandler.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Commands/SendTemplateEmailHandler.cs new file mode 100644 index 00000000..fd046b97 --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Commands/SendTemplateEmailHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using Coderr.Server.Api.Core.Messaging; +using Coderr.Server.Api.Core.Messaging.Commands; +using Coderr.Server.App.Modules.Messaging.Templating; +using DotNetCqs; +using Newtonsoft.Json.Linq; + + +namespace Coderr.Server.App.Modules.Messaging.Commands +{ + /// + /// Send an email using a template. + /// + public class SendTemplateEmailHandler : IMessageHandler + { + + /// + /// Execute a command asynchronously. + /// + /// Command to execute. + /// + /// Task which will be completed once the command has been executed. + /// + public async Task HandleAsync(IMessageContext context, SendTemplateEmail command) + { + var loader = new TemplateLoader(); + var templateParser = new TemplateParser(); + + var layout = loader.Load("Layout"); + + var template = loader.Load(command.TemplateName); + + var html = templateParser.RunAll(template, command.Model); + if (html.IndexOf("src=\"cid:", StringComparison.OrdinalIgnoreCase) == -1) + html = html.Replace(@"src=""", @"src=""cid:"); + + string complete; + try + { + complete = templateParser.RunFormatterOnly(layout, + new {Title = command.MailTitle, Body = html}); + } + catch (Exception) + { + throw; + } + + var msg = new EmailMessage(command.To) {Subject = command.Subject, HtmlBody = complete}; + + foreach (var resource in template.Resources) + { + var buffer = new byte[resource.Value.Length]; + resource.Value.Read(buffer, 0, buffer.Length); + resource.Value.Position = 0; + var linkedResource = new EmailResource(resource.Key, buffer); + + var reader = new BinaryReader(resource.Value); + var dimensions = ImageHelper.GetDimensions(reader); + var key = string.Format("src=\"cid:{0}\"", resource.Key); + complete = complete.Replace(key, + string.Format("{0} width=\"{1}\" height=\"{2}\" style=\"border: 1px solid #000\"", key, + dimensions.Width, dimensions.Height)); + resource.Value.Position = 0; + + msg.Resources.Add(linkedResource); + } + foreach (var resource in layout.Resources) + { + var buffer = new byte[resource.Value.Length]; + resource.Value.Read(buffer, 0, buffer.Length); + resource.Value.Position = 0; + var linkedResource = new EmailResource(resource.Key, buffer); + + var reader = new BinaryReader(resource.Value); + var dimensions = ImageHelper.GetDimensions(reader); + var key = string.Format("src=\"cid:{0}\"", resource.Key); + complete = complete.Replace(key, + string.Format("{0} width=\"{1}\" height=\"{2}\"", key, dimensions.Width, dimensions.Height)); + resource.Value.Position = 0; + + msg.Resources.Add(linkedResource); + } + if (command.Resources != null) + { + foreach (var resource in command.Resources) + { + msg.Resources.Add(resource); + } + } + msg.HtmlBody = complete; + + var sendEmail = new SendEmail(msg); + await context.SendAsync(sendEmail); + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/ITemplateParser.cs b/src/Server/Coderr.Server.App/Modules/Messaging/ITemplateParser.cs similarity index 92% rename from src/Server/OneTrueError.App/Modules/Messaging/ITemplateParser.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/ITemplateParser.cs index 9d3cc12d..3c91f8f7 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/ITemplateParser.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/ITemplateParser.cs @@ -1,24 +1,24 @@ -namespace OneTrueError.App.Modules.Messaging -{ - /// - /// Generates HTML from a text template. - /// - public interface ITemplateParser - { - /// - /// Run all transformation steps. - /// - /// Template to transform - /// View model - /// Generated HTML - string RunAll(Template messageTemplate, object viewModel); - - /// - /// Format keywords only. i.e. no HTML transformation etc. - /// - /// Template to transform - /// View model - /// Generated HTML - string RunFormatterOnly(Template messageTemplate, object viewModel); - } +namespace Coderr.Server.App.Modules.Messaging +{ + /// + /// Generates HTML from a text template. + /// + public interface ITemplateParser + { + /// + /// Run all transformation steps. + /// + /// Template to transform + /// View model + /// Generated HTML + string RunAll(Template messageTemplate, object viewModel); + + /// + /// Format keywords only. i.e. no HTML transformation etc. + /// + /// Template to transform + /// View model + /// Generated HTML + string RunFormatterOnly(Template messageTemplate, object viewModel); + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Template.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Template.cs similarity index 88% rename from src/Server/OneTrueError.App/Modules/Messaging/Template.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Template.cs index 4cc2d721..029a9ff0 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Template.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Template.cs @@ -1,21 +1,21 @@ -using System.Collections.Generic; -using System.IO; - -namespace OneTrueError.App.Modules.Messaging -{ - /// - /// Message template (contains markdown, view model instructions etc). - /// - public class Template - { - /// - /// Template content - /// - public Stream Content { get; set; } - - /// - /// Resources embedded in the template. - /// - public Dictionary Resources { get; set; } - } +using System.Collections.Generic; +using System.IO; + +namespace Coderr.Server.App.Modules.Messaging +{ + /// + /// Message template (contains markdown, view model instructions etc). + /// + public class Template + { + /// + /// Template content + /// + public Stream Content { get; set; } + + /// + /// Resources embedded in the template. + /// + public Dictionary Resources { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/DateFormatter.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/DateFormatter.cs similarity index 95% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/DateFormatter.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/DateFormatter.cs index a9f907aa..5abfd38b 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/DateFormatter.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/DateFormatter.cs @@ -1,82 +1,82 @@ -using System; - -namespace OneTrueError.App.Modules.Messaging.Templating -{ - /// - /// Used to format dates into different representations like duration from now. - /// - public static class DateFormatter - { - /// - /// Display elapsed time from the given date - /// - /// Time to diff from (local time) - /// English string - public static string ElapsedTime(DateTime specifiedTime) - { - var difference = DateTime.Now.Subtract(specifiedTime); - - var years = (int) (difference.TotalDays/365); - if (years >= 1) - return string.Format("{0} {1} ago", years, years == 1 ? "year" : "years"); - - var months = (int) (difference.TotalDays/30); - if (months >= 1) - return string.Format("{0} {1} ago", months, months == 1 ? "month" : "months"); - - var weeks = (int) (difference.TotalDays/7); - if (weeks >= 1) - return string.Format("{0} {1} ago", weeks, weeks == 1 ? "week" : "weeks"); - - var days = (int) difference.TotalDays; - if (days >= 1) - return string.Format("{0} {1} ago", days, days == 1 ? "day" : "days"); - - var hours = (int) difference.TotalHours; - if (hours >= 1) - return string.Format("{0} {1} ago", hours, hours == 1 ? "hour" : "hours"); - - var minutes = (int) difference.TotalMinutes; - if (minutes >= 1) - return string.Format("{0} {1} ago", minutes, minutes == 1 ? "minute" : "minutes"); - - return "moments ago"; - } - - /// - /// Generates a text representing the period of time from now to the given future date - /// - /// Date in the future (local time) - /// For instance "in two days" - public static string FutureTime(DateTime specifiedTime) - { - var difference = DateTime.Now.Subtract(specifiedTime); - - var years = (int) (difference.TotalDays/365); - if (years >= 1) - return string.Format("in {0} {1}", years, years == 1 ? "year" : "years"); - - var months = (int) (difference.TotalDays/30); - if (months >= 1) - return string.Format("in {0} {1}", months, months == 1 ? "month" : "months"); - - var weeks = (int) (difference.TotalDays/7); - if (weeks >= 1) - return string.Format("in {0} {1}", weeks, weeks == 1 ? "week" : "weeks"); - - var days = (int) difference.TotalDays; - if (days >= 1) - return string.Format("in {0} {1}", days, days == 1 ? "day" : "days"); - - var hours = (int) difference.TotalHours; - if (hours >= 1) - return string.Format("in {0} {1}", hours, hours == 1 ? "hour" : "hours"); - - var minutes = (int) difference.TotalMinutes; - if (minutes >= 1) - return string.Format("in {0} {1}", minutes, minutes == 1 ? "minute" : "minutes"); - - return "in a moment"; - } - } +using System; + +namespace Coderr.Server.App.Modules.Messaging.Templating +{ + /// + /// Used to format dates into different representations like duration from now. + /// + public static class DateFormatter + { + /// + /// Display elapsed time from the given date + /// + /// Time to diff from (local time) + /// English string + public static string ElapsedTime(DateTime specifiedTime) + { + var difference = DateTime.Now.Subtract(specifiedTime); + + var years = (int) (difference.TotalDays/365); + if (years >= 1) + return string.Format("{0} {1} ago", years, years == 1 ? "year" : "years"); + + var months = (int) (difference.TotalDays/30); + if (months >= 1) + return string.Format("{0} {1} ago", months, months == 1 ? "month" : "months"); + + var weeks = (int) (difference.TotalDays/7); + if (weeks >= 1) + return string.Format("{0} {1} ago", weeks, weeks == 1 ? "week" : "weeks"); + + var days = (int) difference.TotalDays; + if (days >= 1) + return string.Format("{0} {1} ago", days, days == 1 ? "day" : "days"); + + var hours = (int) difference.TotalHours; + if (hours >= 1) + return string.Format("{0} {1} ago", hours, hours == 1 ? "hour" : "hours"); + + var minutes = (int) difference.TotalMinutes; + if (minutes >= 1) + return string.Format("{0} {1} ago", minutes, minutes == 1 ? "minute" : "minutes"); + + return "moments ago"; + } + + /// + /// Generates a text representing the period of time from now to the given future date + /// + /// Date in the future (local time) + /// For instance "in two days" + public static string FutureTime(DateTime specifiedTime) + { + var difference = DateTime.Now.Subtract(specifiedTime); + + var years = (int) (difference.TotalDays/365); + if (years >= 1) + return string.Format("in {0} {1}", years, years == 1 ? "year" : "years"); + + var months = (int) (difference.TotalDays/30); + if (months >= 1) + return string.Format("in {0} {1}", months, months == 1 ? "month" : "months"); + + var weeks = (int) (difference.TotalDays/7); + if (weeks >= 1) + return string.Format("in {0} {1}", weeks, weeks == 1 ? "week" : "weeks"); + + var days = (int) difference.TotalDays; + if (days >= 1) + return string.Format("in {0} {1}", days, days == 1 ? "day" : "days"); + + var hours = (int) difference.TotalHours; + if (hours >= 1) + return string.Format("in {0} {1}", hours, hours == 1 ? "hour" : "hours"); + + var minutes = (int) difference.TotalMinutes; + if (minutes >= 1) + return string.Format("in {0} {1}", minutes, minutes == 1 ? "minute" : "minutes"); + + return "in a moment"; + } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/JObjectReflector.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/JObjectReflector.cs new file mode 100644 index 00000000..c5808284 --- /dev/null +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/JObjectReflector.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +namespace Coderr.Server.App.Modules.Messaging.Templating.Formatting +{ + public class JObjectReflector + { + private readonly Dictionary _items = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// Checks if the specified type could be traversed or just added as a value. + /// + /// Type to check + /// true if we should add this type as a value; false if we should do reflection on it. + public static bool IsSimpleType(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + return type.IsPrimitive + || type == typeof(decimal) + || type == typeof(string) + || type == typeof(DateTime) + || type == typeof(int) + || type == typeof(DateTimeOffset) + || type == typeof(TimeSpan); + } + + public IDictionary Reflect(JObject data) + { + if (data == null) throw new ArgumentNullException(nameof(data)); + + _items.Clear(); + foreach (var prop in data) ReflectObject(prop.Value, prop.Key); + + return _items; + } + + private void ReflectObject(JArray data, string prefix) + { + var index = 0; + foreach (var item in data) + { + var childPrefix = $"{prefix}[{index++}]"; + ReflectObject(item, childPrefix); + } + } + + private void ReflectObject(JToken data, string prefix) + { + var isHandled = true; + switch (data.Type) + { + case JTokenType.Array: + ReflectObject((JArray) data, prefix); + break; + case JTokenType.Date: + _items.Add(prefix, data.ToObject()); + break; + + case JTokenType.Boolean: + _items.Add(prefix, data.ToObject()); + break; + case JTokenType.Float: + _items.Add(prefix, data.ToObject()); + break; + case JTokenType.Guid: + _items.Add(prefix, data.ToObject()); + break; + case JTokenType.Integer: + _items.Add(prefix, data.ToObject()); + break; + case JTokenType.String: + _items.Add(prefix, data.ToObject()); + break; + case JTokenType.TimeSpan: + _items.Add(prefix, data.ToObject()); + break; + case JTokenType.Uri: + _items.Add(prefix, data.ToObject()); + break; + case JTokenType.Null: + _items.Add(prefix, null); + break; + default: + isHandled = false; + break; + } + + if (isHandled) + return; + + if (data is JObject) + foreach (var prop in (JObject) data) + ReflectObject(prop.Value, $"{prefix}.{prop.Key}"); + } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/ObjectToDictionaryConverter.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/ObjectToDictionaryConverter.cs similarity index 94% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/ObjectToDictionaryConverter.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/ObjectToDictionaryConverter.cs index 04a67b69..c089aebf 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/ObjectToDictionaryConverter.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/ObjectToDictionaryConverter.cs @@ -1,88 +1,88 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.App.Modules.Messaging.Templating.Formatting -{ - /// - /// Converts an object into a dictionary (to be able to process it in the template) - /// - public class ObjectToDictionaryConverter - { - /// - /// Turn an object into a string which can be used for debugging. - /// - /// Object to get a string representation for - /// "null" if the object is null, otherwise an string as given per object sample - /// - /// Look at the class doc for an example. - /// - public Dictionary Convert(object instance) - { - if (instance == null) - throw new ArgumentNullException("instance"); - - var dictionary = new Dictionary(); - ReflectObject(instance, "", dictionary); - - return dictionary; - } - - /// - /// Checks if the specified type could be traversed or just added as a value. - /// - /// Type to check - /// true if we should add this type as a value; false if we should do reflection on it. - public static bool IsSimpleType(Type type) - { - if (type == null) throw new ArgumentNullException("type"); - - return type.IsPrimitive - || type == typeof(decimal) - || type == typeof(string) - || type == typeof(DateTime) - || type == typeof(int) - || type == typeof(DateTimeOffset) - || type == typeof(TimeSpan); - } - - /// - /// Use reflection on a complex object to add it's values to our context collection - /// - /// Current object to reflect - /// Prefix, like "User.Address.Postal.ZipCode" - /// Collection that values should be added to. - [SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength", - Justification = "Null check is done on the argument.")] - protected void ReflectObject(object instance, string prefix, IDictionary dictionary) - { - if (instance == null) throw new ArgumentNullException("instance"); - if (prefix == null) throw new ArgumentNullException("prefix"); - if (dictionary == null) throw new ArgumentNullException("dictionary"); - - foreach (var propInfo in instance.GetType().GetProperties()) - { - //TODO: Add support. - if (propInfo.GetIndexParameters().Length != 0) - continue; - - var value = propInfo.GetValue(instance, null); - if (value == null) - { - dictionary.Add(prefix + propInfo.Name, ""); - continue; - } - - if (IsSimpleType(value.GetType())) - { - dictionary.Add(prefix + propInfo.Name, value.ToString()); - } - else - { - var newPrefix = prefix == "" ? propInfo.Name + "." : prefix + propInfo.Name + "."; - ReflectObject(value, newPrefix, dictionary); - } - } - } - } +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Coderr.Server.App.Modules.Messaging.Templating.Formatting +{ + /// + /// Converts an object into a dictionary (to be able to process it in the template) + /// + public class ObjectToDictionaryConverter + { + /// + /// Turn an object into a string which can be used for debugging. + /// + /// Object to get a string representation for + /// "null" if the object is null, otherwise an string as given per object sample + /// + /// Look at the class doc for an example. + /// + public IDictionary Convert(object instance) + { + if (instance == null) + throw new ArgumentNullException("instance"); + + var dictionary = new Dictionary(); + ReflectObject(instance, "", dictionary); + + return dictionary; + } + + /// + /// Checks if the specified type could be traversed or just added as a value. + /// + /// Type to check + /// true if we should add this type as a value; false if we should do reflection on it. + public static bool IsSimpleType(Type type) + { + if (type == null) throw new ArgumentNullException("type"); + + return type.IsPrimitive + || type == typeof(decimal) + || type == typeof(string) + || type == typeof(DateTime) + || type == typeof(int) + || type == typeof(DateTimeOffset) + || type == typeof(TimeSpan); + } + + /// + /// Use reflection on a complex object to add it's values to our context collection + /// + /// Current object to reflect + /// Prefix, like "User.Address.Postal.ZipCode" + /// Collection that values should be added to. + [SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength", + Justification = "Null check is done on the argument.")] + protected void ReflectObject(object instance, string prefix, IDictionary dictionary) + { + if (instance == null) throw new ArgumentNullException("instance"); + if (prefix == null) throw new ArgumentNullException("prefix"); + if (dictionary == null) throw new ArgumentNullException("dictionary"); + + foreach (var propInfo in instance.GetType().GetProperties()) + { + //TODO: Add support. + if (propInfo.GetIndexParameters().Length != 0) + continue; + + var value = propInfo.GetValue(instance, null); + if (value == null) + { + dictionary.Add(prefix + propInfo.Name, ""); + continue; + } + + if (IsSimpleType(value.GetType())) + { + dictionary.Add(prefix + propInfo.Name, value.ToString()); + } + else + { + var newPrefix = prefix == "" ? propInfo.Name + "." : prefix + propInfo.Name + "."; + ReflectObject(value, newPrefix, dictionary); + } + } + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/StringFormatExtensions.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/StringFormatExtensions.cs similarity index 87% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/StringFormatExtensions.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/StringFormatExtensions.cs index 39cd9f5d..8c7859a6 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/StringFormatExtensions.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/StringFormatExtensions.cs @@ -1,20 +1,20 @@ -namespace OneTrueError.App.Modules.Messaging.Templating.Formatting -{ - /// - /// Extension methods for a string - /// - public static class StringFormatExtensions - { - /// - /// Format string - /// - /// string to format - /// arguments to replace with - /// formatted string - public static string FormatWith(this string format, params object[] arguments) - { - var compiler = new StringFormatter(); - return compiler.Format(format, arguments); - } - } +namespace Coderr.Server.App.Modules.Messaging.Templating.Formatting +{ + /// + /// Extension methods for a string + /// + public static class StringFormatExtensions + { + /// + /// Format string + /// + /// string to format + /// arguments to replace with + /// formatted string + public static string FormatWith(this string format, params object[] arguments) + { + var compiler = new StringFormatter(); + return compiler.Format(format, arguments); + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/StringFormatter.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/StringFormatter.cs similarity index 77% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/StringFormatter.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/StringFormatter.cs index 7730327e..3dad35e4 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/StringFormatter.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/StringFormatter.cs @@ -1,67 +1,80 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Text; - -namespace OneTrueError.App.Modules.Messaging.Templating.Formatting -{ - /// - /// Converts a string - /// - public class StringFormatter - { - /// - /// Format a string - /// - /// string to format - /// arguments to replace - /// formatted string - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] - public string Format(string source, params object[] arguments) - { - if (source == null) throw new ArgumentNullException("source"); - if (arguments == null) throw new ArgumentNullException("arguments"); - if (arguments.Length == 0) - throw new ArgumentException("Must provide at least one argument."); - - var tokenizer = new Tokenizer(); - var tokens = tokenizer.Parse(source); - - //Console.WriteLine(string.Join("+", tokens.Select(x => x.Name + x.Value))); - var converter = new ObjectToDictionaryConverter(); - var model = converter.Convert(arguments[0]); - - if (arguments.Length != 1) - { - return ""; - } - - var includeEnd = false; - var sb = new StringBuilder(); - foreach (var token in tokens) - { - if (includeEnd) - { - sb.Append('}'); - includeEnd = false; - } - else if (token.Name != null && token.Name.IndexOfAny(new[] {' ', ',', '-', '+', ':'}) > -1) - { - // vars can't contain spaces. - sb.Append('{'); - sb.Append(token.Name); - includeEnd = true; - } - else if (token.Name == null) - sb.Append(token.Value); - else - { - object value; - if (!model.TryGetValue(token.Name, out value)) - throw new FormatException("Failed to find '" + token.Name + "' in model"); - sb.Append(value); - } - } - return sb.ToString(); - } - } +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Newtonsoft.Json.Linq; + +namespace Coderr.Server.App.Modules.Messaging.Templating.Formatting +{ + /// + /// Converts a string + /// + public class StringFormatter + { + /// + /// Format a string + /// + /// string to format + /// arguments to replace + /// formatted string + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] + public string Format(string source, params object[] arguments) + { + if (source == null) throw new ArgumentNullException("source"); + if (arguments == null) throw new ArgumentNullException("arguments"); + if (arguments.Length == 0) + throw new ArgumentException("Must provide at least one argument."); + + var tokenizer = new Tokenizer(); + var tokens = tokenizer.Parse(source); + + IDictionary model; + //Console.WriteLine(string.Join("+", tokens.Select(x => x.Name + x.Value))); + if (arguments[0] is JObject) + { + var converter = new JObjectReflector(); + model = converter.Reflect((JObject)arguments[0]); + } + else + { + var converter = new ObjectToDictionaryConverter(); + model = converter.Convert(arguments[0]); + } + + + if (arguments.Length != 1) + { + return ""; + } + + var includeEnd = false; + var sb = new StringBuilder(); + foreach (var token in tokens) + { + if (includeEnd) + { + sb.Append('}'); + includeEnd = false; + } + else if (token.Name != null && token.Name.IndexOfAny(new[] { ' ', ',', '-', '+', ':' }) > -1) + { + // vars can't contain spaces. + sb.Append('{'); + sb.Append(token.Name); + includeEnd = true; + } + else if (token.Name == null) + sb.Append(token.Value); + else + { + object value; + if (!model.TryGetValue(token.Name, out value)) + throw new FormatException("Failed to find '" + token.Name + "' in model"); + sb.Append(value); + } + } + return sb.ToString(); + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/Token.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/Token.cs similarity index 92% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/Token.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/Token.cs index c4e4d4e7..993f2c15 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/Token.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/Token.cs @@ -1,46 +1,46 @@ -using System; - -namespace OneTrueError.App.Modules.Messaging.Templating.Formatting -{ - /// - /// A token when parsing the template text - /// - public class Token - { - /// - /// Creates a new instance of . - /// - /// value - /// value - public Token(string value) - { - if (value == null) throw new ArgumentNullException("value"); - Value = value; - } - - /// - /// Creates a new instance of . - /// - /// name - /// value - /// name; value - public Token(string name, string value) - { - if (name == null) throw new ArgumentNullException("name"); - if (value == null) throw new ArgumentNullException("value"); - Name = name; - Value = value; - } - - /// - /// Name (if this is an argument) - /// - public string Name { get; private set; } - - - /// - /// Value/Text (text for non arguments, and the argument value for arguments) - /// - public string Value { get; private set; } - } +using System; + +namespace Coderr.Server.App.Modules.Messaging.Templating.Formatting +{ + /// + /// A token when parsing the template text + /// + public class Token + { + /// + /// Creates a new instance of . + /// + /// value + /// value + public Token(string value) + { + if (value == null) throw new ArgumentNullException("value"); + Value = value; + } + + /// + /// Creates a new instance of . + /// + /// name + /// value + /// name; value + public Token(string name, string value) + { + if (name == null) throw new ArgumentNullException("name"); + if (value == null) throw new ArgumentNullException("value"); + Name = name; + Value = value; + } + + /// + /// Name (if this is an argument) + /// + public string Name { get; private set; } + + + /// + /// Value/Text (text for non arguments, and the argument value for arguments) + /// + public string Value { get; private set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/Tokenizer.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/Tokenizer.cs similarity index 95% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/Tokenizer.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/Tokenizer.cs index ebe01aa5..b5c145fb 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Formatting/Tokenizer.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Formatting/Tokenizer.cs @@ -1,153 +1,153 @@ -using System.Collections.Generic; - -namespace OneTrueError.App.Modules.Messaging.Templating.Formatting -{ - /// - /// Converts a string with named arguments to a string - /// - /// - /// - /// Supports escaping "hello {{world}}!" would produce only text tokens (i.e. no argument name was - /// detected) - /// - /// - /// - /// - /// var tokenizer = new Tokenizer(); - /// var tokens = tokenizer.Parse("Hello {world}!"); - /// Console.WriteLine(tokens[0].Value); // --> "Hello " - /// Console.WriteLine(tokens[1].Name); // --> "world" - /// Console.WriteLine(tokens[2].Value); // --> "!" - /// - /// - public class Tokenizer - { - private readonly List _tokens = new List(); - - /// - /// Delimiters which makes switches from argument name to text when found inside of "{}". - /// - private readonly char[] NameDelimiters = {'{', '}', ' ', '-', '+', '/', '\t', ',', ':'}; - - private bool _isInCurly; - private int _offset; - private int _textStartPos = -1; - private string _value; - - private char Current - { - get { return _value[_offset]; } - } - - private char Next - { - get { return _offset + 1 < _value.Length ? _value[_offset + 1] : char.MinValue; } - } - - /// - /// Parse string - /// - /// Text to parse - /// Tokens found in the string - public IList Parse(string text) - { - _value = text; - while (_offset < _value.Length) - { - switch (Current) - { - case '{': - ParseStartCurly(); - break; - case '}': - ParseEndCurly(); - break; - default: - ParseChar(); - break; - } - - ++_offset; - } - - if (_textStartPos != -1) - { - _tokens.Add(new Token(_value.Substring(_textStartPos))); - } - - return _tokens; - } - - private void ParseChar() - { - if (_textStartPos == -1) - _textStartPos = _offset; - } - - private void ParseEndCurly() - { - if (_textStartPos != -1) - { - // not a valid argument name, just continue treating this single - // end curly as text. - if (Next != '}' && !_isInCurly) - return; - - if (_isInCurly) - _tokens.Add(new Token(_value.Substring(_textStartPos, _offset - _textStartPos), "")); - else - _tokens.Add(new Token(_value.Substring(_textStartPos, _offset - _textStartPos))); - - _textStartPos = -1; - } - - if (_isInCurly) - { - _isInCurly = false; - } - else - { - _tokens.Add(new Token("}")); - if (Next == '}') - _offset++; - } - } - - private void ParseStartCurly() - { - if (Next == '{') - { - if (_textStartPos != -1) - { - _tokens.Add(new Token(_value.Substring(_textStartPos, _offset - _textStartPos) + "{")); - _textStartPos = -1; - } - else - _tokens.Add(new Token("{")); - - _offset++; - return; - } - - // check if this really is for an argument - var pos = _value.IndexOfAny(NameDelimiters, _offset + 1); - if (pos != -1 && _value[pos] != '}') - { - if (_textStartPos == -1) - _textStartPos = _offset; - - //found another char, let's continue searching - return; - } - - if (_textStartPos != -1) - { - _tokens.Add(new Token(_value.Substring(_textStartPos, _offset - _textStartPos))); - _textStartPos = -1; - } - - _textStartPos = _offset + 1; - _isInCurly = true; - } - } +using System.Collections.Generic; + +namespace Coderr.Server.App.Modules.Messaging.Templating.Formatting +{ + /// + /// Converts a string with named arguments to a string + /// + /// + /// + /// Supports escaping "hello {{world}}!" would produce only text tokens (i.e. no argument name was + /// detected) + /// + /// + /// + /// + /// var tokenizer = new Tokenizer(); + /// var tokens = tokenizer.Parse("Hello {world}!"); + /// Console.WriteLine(tokens[0].Value); // --> "Hello " + /// Console.WriteLine(tokens[1].Name); // --> "world" + /// Console.WriteLine(tokens[2].Value); // --> "!" + /// + /// + public class Tokenizer + { + private readonly List _tokens = new List(); + + /// + /// Delimiters which makes switches from argument name to text when found inside of "{}". + /// + private readonly char[] NameDelimiters = {'{', '}', ' ', '-', '+', '/', '\t', ',', ':'}; + + private bool _isInCurly; + private int _offset; + private int _textStartPos = -1; + private string _value; + + private char Current + { + get { return _value[_offset]; } + } + + private char Next + { + get { return _offset + 1 < _value.Length ? _value[_offset + 1] : char.MinValue; } + } + + /// + /// Parse string + /// + /// Text to parse + /// Tokens found in the string + public IList Parse(string text) + { + _value = text; + while (_offset < _value.Length) + { + switch (Current) + { + case '{': + ParseStartCurly(); + break; + case '}': + ParseEndCurly(); + break; + default: + ParseChar(); + break; + } + + ++_offset; + } + + if (_textStartPos != -1) + { + _tokens.Add(new Token(_value.Substring(_textStartPos))); + } + + return _tokens; + } + + private void ParseChar() + { + if (_textStartPos == -1) + _textStartPos = _offset; + } + + private void ParseEndCurly() + { + if (_textStartPos != -1) + { + // not a valid argument name, just continue treating this single + // end curly as text. + if (Next != '}' && !_isInCurly) + return; + + if (_isInCurly) + _tokens.Add(new Token(_value.Substring(_textStartPos, _offset - _textStartPos), "")); + else + _tokens.Add(new Token(_value.Substring(_textStartPos, _offset - _textStartPos))); + + _textStartPos = -1; + } + + if (_isInCurly) + { + _isInCurly = false; + } + else + { + _tokens.Add(new Token("}")); + if (Next == '}') + _offset++; + } + } + + private void ParseStartCurly() + { + if (Next == '{') + { + if (_textStartPos != -1) + { + _tokens.Add(new Token(_value.Substring(_textStartPos, _offset - _textStartPos) + "{")); + _textStartPos = -1; + } + else + _tokens.Add(new Token("{")); + + _offset++; + return; + } + + // check if this really is for an argument + var pos = _value.IndexOfAny(NameDelimiters, _offset + 1); + if (pos != -1 && _value[pos] != '}') + { + if (_textStartPos == -1) + _textStartPos = _offset; + + //found another char, let's continue searching + return; + } + + if (_textStartPos != -1) + { + _tokens.Add(new Token(_value.Substring(_textStartPos, _offset - _textStartPos))); + _textStartPos = -1; + } + + _textStartPos = _offset + 1; + _isInCurly = true; + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/ImageHelper.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/ImageHelper.cs similarity index 96% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/ImageHelper.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/ImageHelper.cs index 98b7d87d..5783e2e1 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/ImageHelper.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/ImageHelper.cs @@ -1,158 +1,158 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace OneTrueError.App.Modules.Messaging.Templating -{ - /// - /// http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file - /// - public static class ImageHelper - { - private const string ErrorMessage = "Could not recognise image format."; - - private static readonly Dictionary> imageFormatDecoders = new Dictionary - > - { - {new byte[] {0x42, 0x4D}, DecodeBitmap}, - {new byte[] {0x47, 0x49, 0x46, 0x38, 0x37, 0x61}, DecodeGif}, - {new byte[] {0x47, 0x49, 0x46, 0x38, 0x39, 0x61}, DecodeGif}, - {new byte[] {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, DecodePng}, - {new byte[] {0xff, 0xd8}, DecodeJfif} - }; - - /// - /// Gets the dimensions of an image. - /// - /// The path of the image to get the dimensions of. - /// The dimensions of the specified image. - /// The image was of an unrecognised format. - public static Size GetDimensions(string path) - { - using (var binaryReader = new BinaryReader(File.OpenRead(path))) - { - try - { - return GetDimensions(binaryReader); - } - catch (ArgumentException e) - { - if (e.Message.StartsWith(ErrorMessage)) - { - throw new ArgumentException(ErrorMessage, "path", e); - } - throw; - } - } - } - - /// - /// Gets the dimensions of an image. - /// - /// reader pointing at the start of the image. - /// The dimensions of the specified image. - /// The image was of an unrecognised format. - public static Size GetDimensions(BinaryReader binaryReader) - { - if (binaryReader == null) throw new ArgumentNullException("binaryReader"); - - var maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length; - - var magicBytes = new byte[maxMagicBytesLength]; - - for (var i = 0; i < maxMagicBytesLength; i += 1) - { - magicBytes[i] = binaryReader.ReadByte(); - - foreach (var kvPair in imageFormatDecoders) - { - if (magicBytes.StartsWith(kvPair.Key)) - { - return kvPair.Value(binaryReader); - } - } - } - - throw new ArgumentException(ErrorMessage, "binaryReader"); - } - - private static Size DecodeBitmap(BinaryReader binaryReader) - { - binaryReader.ReadBytes(16); - var width = binaryReader.ReadInt32(); - var height = binaryReader.ReadInt32(); - return new Size(width, height); - } - - private static Size DecodeGif(BinaryReader binaryReader) - { - int width = binaryReader.ReadInt16(); - int height = binaryReader.ReadInt16(); - return new Size(width, height); - } - - // ReSharper disable once IdentifierTypo - private static Size DecodeJfif(BinaryReader binaryReader) - { - while (binaryReader.ReadByte() == 0xff) - { - var marker = binaryReader.ReadByte(); - short chunkLength = binaryReader.ReadLittleEndianInt16(); - - if (marker == 0xc0) - { - binaryReader.ReadByte(); - - int height = binaryReader.ReadLittleEndianInt16(); - int width = binaryReader.ReadLittleEndianInt16(); - return new Size(width, height); - } - - binaryReader.ReadBytes(chunkLength - 2); - } - - throw new ArgumentException(ErrorMessage); - } - - private static Size DecodePng(BinaryReader binaryReader) - { - binaryReader.ReadBytes(8); - int width = binaryReader.ReadLittleEndianInt32(); - int height = binaryReader.ReadLittleEndianInt32(); - return new Size(width, height); - } - - private static short ReadLittleEndianInt16(this BinaryReader binaryReader) - { - var bytes = new byte[sizeof(short)]; - for (var i = 0; i < sizeof(short); i += 1) - { - bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte(); - } - return BitConverter.ToInt16(bytes, 0); - } - - private static int ReadLittleEndianInt32(this BinaryReader binaryReader) - { - var bytes = new byte[sizeof(int)]; - for (var i = 0; i < sizeof(int); i += 1) - { - bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte(); - } - return BitConverter.ToInt32(bytes, 0); - } - - private static bool StartsWith(this byte[] thisBytes, byte[] thatBytes) - { - for (var i = 0; i < thatBytes.Length; i += 1) - { - if (thisBytes[i] != thatBytes[i]) - { - return false; - } - } - return true; - } - } +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Coderr.Server.App.Modules.Messaging.Templating +{ + /// + /// http://stackoverflow.com/questions/111345/getting-image-dimensions-without-reading-the-entire-file + /// + public static class ImageHelper + { + private const string ErrorMessage = "Could not recognise image format."; + + private static readonly Dictionary> imageFormatDecoders = new Dictionary + > + { + {new byte[] {0x42, 0x4D}, DecodeBitmap}, + {new byte[] {0x47, 0x49, 0x46, 0x38, 0x37, 0x61}, DecodeGif}, + {new byte[] {0x47, 0x49, 0x46, 0x38, 0x39, 0x61}, DecodeGif}, + {new byte[] {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}, DecodePng}, + {new byte[] {0xff, 0xd8}, DecodeJfif} + }; + + /// + /// Gets the dimensions of an image. + /// + /// The path of the image to get the dimensions of. + /// The dimensions of the specified image. + /// The image was of an unrecognised format. + public static Size GetDimensions(string path) + { + using (var binaryReader = new BinaryReader(File.OpenRead(path))) + { + try + { + return GetDimensions(binaryReader); + } + catch (ArgumentException e) + { + if (e.Message.StartsWith(ErrorMessage)) + { + throw new ArgumentException(ErrorMessage, "path", e); + } + throw; + } + } + } + + /// + /// Gets the dimensions of an image. + /// + /// reader pointing at the start of the image. + /// The dimensions of the specified image. + /// The image was of an unrecognised format. + public static Size GetDimensions(BinaryReader binaryReader) + { + if (binaryReader == null) throw new ArgumentNullException("binaryReader"); + + var maxMagicBytesLength = imageFormatDecoders.Keys.OrderByDescending(x => x.Length).First().Length; + + var magicBytes = new byte[maxMagicBytesLength]; + + for (var i = 0; i < maxMagicBytesLength; i += 1) + { + magicBytes[i] = binaryReader.ReadByte(); + + foreach (var kvPair in imageFormatDecoders) + { + if (magicBytes.StartsWith(kvPair.Key)) + { + return kvPair.Value(binaryReader); + } + } + } + + throw new ArgumentException(ErrorMessage, "binaryReader"); + } + + private static Size DecodeBitmap(BinaryReader binaryReader) + { + binaryReader.ReadBytes(16); + var width = binaryReader.ReadInt32(); + var height = binaryReader.ReadInt32(); + return new Size(width, height); + } + + private static Size DecodeGif(BinaryReader binaryReader) + { + int width = binaryReader.ReadInt16(); + int height = binaryReader.ReadInt16(); + return new Size(width, height); + } + + // ReSharper disable once IdentifierTypo + private static Size DecodeJfif(BinaryReader binaryReader) + { + while (binaryReader.ReadByte() == 0xff) + { + var marker = binaryReader.ReadByte(); + short chunkLength = binaryReader.ReadLittleEndianInt16(); + + if (marker == 0xc0) + { + binaryReader.ReadByte(); + + int height = binaryReader.ReadLittleEndianInt16(); + int width = binaryReader.ReadLittleEndianInt16(); + return new Size(width, height); + } + + binaryReader.ReadBytes(chunkLength - 2); + } + + throw new ArgumentException(ErrorMessage); + } + + private static Size DecodePng(BinaryReader binaryReader) + { + binaryReader.ReadBytes(8); + int width = binaryReader.ReadLittleEndianInt32(); + int height = binaryReader.ReadLittleEndianInt32(); + return new Size(width, height); + } + + private static short ReadLittleEndianInt16(this BinaryReader binaryReader) + { + var bytes = new byte[sizeof(short)]; + for (var i = 0; i < sizeof(short); i += 1) + { + bytes[sizeof(short) - 1 - i] = binaryReader.ReadByte(); + } + return BitConverter.ToInt16(bytes, 0); + } + + private static int ReadLittleEndianInt32(this BinaryReader binaryReader) + { + var bytes = new byte[sizeof(int)]; + for (var i = 0; i < sizeof(int); i += 1) + { + bytes[sizeof(int) - 1 - i] = binaryReader.ReadByte(); + } + return BitConverter.ToInt32(bytes, 0); + } + + private static bool StartsWith(this byte[] thisBytes, byte[] thatBytes) + { + for (var i = 0; i < thatBytes.Length; i += 1) + { + if (thisBytes[i] != thatBytes[i]) + { + return false; + } + } + return true; + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Layout/Template.html b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Layout/Template.html similarity index 77% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/Layout/Template.html rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/Layout/Template.html index 8a533acb..066ff637 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Layout/Template.html +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Layout/Template.html @@ -1,295 +1,259 @@ - - - - - - {Title} - - - - - - - - - -
-
- - - - - -
- - -
{Title}
-
-
-
- - - - - - -
- {Body} -
- - - - - - - - + + + + + + {Title} + + + + + + + + + +
+
+ + + + + +
+ + +
{Title}
+
+
+
+ + + + + + +
+ {Body} +
+ + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Layout/logo2.png b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Layout/logo2.png new file mode 100644 index 00000000..494e01a1 Binary files /dev/null and b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Layout/logo2.png differ diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Size.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Size.cs similarity index 91% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/Size.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/Size.cs index 76ff80c5..5395753d 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Size.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/Size.cs @@ -1,34 +1,34 @@ -using System; - -namespace OneTrueError.App.Modules.Messaging.Templating -{ - /// - /// Look at the size of that thing. - /// - public class Size - { - /// - /// Creates a new instance of . - /// - /// width - /// height - /// something is not 1 or larger - public Size(int width, int height) - { - if (width <= 0) throw new ArgumentOutOfRangeException("width"); - if (height <= 0) throw new ArgumentOutOfRangeException("height"); - Width = width; - Height = height; - } - - /// - /// Height in some more arbitrary unit - /// - public int Height { get; set; } - - /// - /// Weight in some arbitrary unit - /// - public int Width { get; set; } - } +using System; + +namespace Coderr.Server.App.Modules.Messaging.Templating +{ + /// + /// Look at the size of that thing. + /// + public class Size + { + /// + /// Creates a new instance of . + /// + /// width + /// height + /// something is not 1 or larger + public Size(int width, int height) + { + if (width <= 0) throw new ArgumentOutOfRangeException("width"); + if (height <= 0) throw new ArgumentOutOfRangeException("height"); + Width = width; + Height = height; + } + + /// + /// Height in some more arbitrary unit + /// + public int Height { get; set; } + + /// + /// Weight in some arbitrary unit + /// + public int Width { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/TemplateLoader.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/TemplateLoader.cs similarity index 96% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/TemplateLoader.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/TemplateLoader.cs index 479612c2..5040df11 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/TemplateLoader.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/TemplateLoader.cs @@ -1,105 +1,105 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Reflection; - -namespace OneTrueError.App.Modules.Messaging.Templating -{ - /// - /// Loads a mail template. - /// - public class TemplateLoader - { - private static readonly Dictionary _templates = - new Dictionary(); - - /// - /// Load template - /// - /// TODO: Describe name/path convention - /// templare if found; otherwise null. - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", - Justification = "The static field is just an implementation detail.")] - public Template Load(string templateName) - { - if (_templates.Count == 0) - LoadTemplates(); - - TemplateMapping mapping; - if (!_templates.TryGetValue(templateName, out mapping)) - throw new ArgumentOutOfRangeException("templateName", templateName, - "Failed to find template '" + templateName + "'."); - - var templateStream = mapping.Assembly.GetManifestResourceStream(mapping.FullResourceName); - - var template = new Template {Content = templateStream, Resources = new Dictionary()}; - foreach (var kvp in mapping.ResourceNames) - { - var resource = mapping.Assembly.GetManifestResourceStream(kvp.Value); - template.Resources.Add(kvp.Key, resource); - } - return template; - } - - private static void LoadTemplates() - { - foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (assembly.IsDynamic) - continue; - - LoadTemplates(assembly); - } - } - - private static void LoadTemplates(Assembly assembly) - { - var resources = assembly.GetManifestResourceNames(); - var templateNames = - resources.Where(x => x.EndsWith(".Template.md", StringComparison.OrdinalIgnoreCase) - || x.EndsWith(".Template.html", StringComparison.OrdinalIgnoreCase)); - - - foreach (var templateName in templateNames) - { - var mapping = new TemplateMapping {FullResourceName = templateName}; - - var toRemove = templateName.EndsWith(".html") - ? "Template.html".Length - : "Template.md".Length; - var ns = templateName.Remove(templateName.Length - toRemove, toRemove); - var fullImageNames = resources.Where(x => x.StartsWith(ns) && x != templateName).ToList(); - var images = new Dictionary(); - foreach (var imagePath in fullImageNames) - { - if (imagePath.EndsWith("Template.md") || imagePath.EndsWith("Template.html")) - continue; - - var imageName = imagePath.Remove(0, ns.Length); - images.Add(imageName, imagePath); - } - - var pos = ns.TrimEnd('.').LastIndexOf('.'); - var name = ns.Substring(pos).Trim('.'); - mapping.TemplateName = name; - mapping.ResourceNames = images; - mapping.Assembly = assembly; - - if (_templates.ContainsKey(templateName)) - throw new InvalidOperationException("Already contains " + templateName); - - _templates[mapping.TemplateName] = mapping; - } - } - - private class TemplateMapping - { - public Assembly Assembly { get; set; } - public string FullResourceName { get; set; } - public Dictionary ResourceNames { get; set; } - public string TemplateName { get; set; } - } - } +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Coderr.Server.App.Modules.Messaging.Templating +{ + /// + /// Loads a mail template. + /// + public class TemplateLoader + { + private static readonly Dictionary _templates = + new Dictionary(); + + /// + /// Load template + /// + /// TODO: Describe name/path convention + /// templare if found; otherwise null. + [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic", + Justification = "The static field is just an implementation detail.")] + public Template Load(string templateName) + { + if (_templates.Count == 0) + LoadTemplates(); + + TemplateMapping mapping; + if (!_templates.TryGetValue(templateName, out mapping)) + throw new ArgumentOutOfRangeException("templateName", templateName, + "Failed to find template '" + templateName + "'."); + + var templateStream = mapping.Assembly.GetManifestResourceStream(mapping.FullResourceName); + + var template = new Template {Content = templateStream, Resources = new Dictionary()}; + foreach (var kvp in mapping.ResourceNames) + { + var resource = mapping.Assembly.GetManifestResourceStream(kvp.Value); + template.Resources.Add(kvp.Key, resource); + } + return template; + } + + private static void LoadTemplates() + { + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assembly.IsDynamic) + continue; + + LoadTemplates(assembly); + } + } + + private static void LoadTemplates(Assembly assembly) + { + var resources = assembly.GetManifestResourceNames(); + var templateNames = + resources.Where(x => x.EndsWith(".Template.md", StringComparison.OrdinalIgnoreCase) + || x.EndsWith(".Template.html", StringComparison.OrdinalIgnoreCase)); + + + foreach (var templateName in templateNames) + { + var mapping = new TemplateMapping {FullResourceName = templateName}; + + var toRemove = templateName.EndsWith(".html") + ? "Template.html".Length + : "Template.md".Length; + var ns = templateName.Remove(templateName.Length - toRemove, toRemove); + var fullImageNames = resources.Where(x => x.StartsWith(ns) && x != templateName).ToList(); + var images = new Dictionary(); + foreach (var imagePath in fullImageNames) + { + if (imagePath.EndsWith("Template.md") || imagePath.EndsWith("Template.html")) + continue; + + var imageName = imagePath.Remove(0, ns.Length); + images.Add(imageName, imagePath); + } + + var pos = ns.TrimEnd('.').LastIndexOf('.'); + var name = ns.Substring(pos).Trim('.'); + mapping.TemplateName = name; + mapping.ResourceNames = images; + mapping.Assembly = assembly; + + if (_templates.ContainsKey(templateName)) + throw new InvalidOperationException("Already contains " + templateName); + + _templates[mapping.TemplateName] = mapping; + } + } + + private class TemplateMapping + { + public Assembly Assembly { get; set; } + public string FullResourceName { get; set; } + public Dictionary ResourceNames { get; set; } + public string TemplateName { get; set; } + } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/TemplateParser.cs b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/TemplateParser.cs similarity index 85% rename from src/Server/OneTrueError.App/Modules/Messaging/Templating/TemplateParser.cs rename to src/Server/Coderr.Server.App/Modules/Messaging/Templating/TemplateParser.cs index a16c47ae..90c2434f 100644 --- a/src/Server/OneTrueError.App/Modules/Messaging/Templating/TemplateParser.cs +++ b/src/Server/Coderr.Server.App/Modules/Messaging/Templating/TemplateParser.cs @@ -1,85 +1,83 @@ -using System; -using System.IO; -using System.Text; -using ColorCode; -using Griffin.Container; -using MarkdownSharp; -using OneTrueError.App.Modules.Messaging.Templating.Formatting; - -namespace OneTrueError.App.Modules.Messaging.Templating -{ - /// - /// Used to parse texts (convert markdown and extras to HTML) - /// - [ContainerService(ContainerLifetime.Transient)] - public class TemplateParser : ITemplateParser - { - /// - /// Run all formatters (replace text tokens, run markdown and finalize with a syntax highlighter) - /// - /// template to parse - /// viewModel used to fill the template. - /// HTML - public string RunAll(Template messageTemplate, object viewModel) - { - if (messageTemplate == null) throw new ArgumentNullException("messageTemplate"); - - var sr = new StreamReader(messageTemplate.Content); - var text = sr.ReadToEnd(); - var formatter = new StringFormatter(); - text = formatter.Format(text, viewModel); - text = Markdown(text); - return ColorizeCode(text); - } - - /// - /// Run only the viewModel replacer. - /// - /// template to parse - /// viewModel used to fill the template. - /// HTML - public string RunFormatterOnly(Template messageTemplate, object viewModel) - { - if (messageTemplate == null) throw new ArgumentNullException("messageTemplate"); - - var sr = new StreamReader(messageTemplate.Content); - var text = sr.ReadToEnd(); - //return Smart.Format(text, viewModel); - var formatter = new StringFormatter(); - return formatter.Format(text, viewModel); - } - - private static string ColorizeCode(string html) - { - var colorizer = new CodeColorizer(); - var sb = new StringBuilder(); - var pos = html.IndexOf("
", StringComparison.Ordinal);
-            if (pos > -1)
-            {
-                var lastPos = 0;
-                sb.Append(html.Substring(0, pos));
-                while (pos > -1)
-                {
-                    lastPos = html.IndexOf("
", pos, StringComparison.Ordinal); - - var snippet = html.Substring(pos + 11, lastPos - pos - 11); - var colorizedSnippet = colorizer.Colorize(snippet, Languages.CSharp); - sb.Append(colorizedSnippet); - - pos = html.IndexOf("
", lastPos, StringComparison.Ordinal);
-                }
-
-                sb.Append(html.Substring(lastPos + 13));
-                html = sb.ToString();
-            }
-
-            return html;
-        }
-
-        private static string Markdown(string template)
-        {
-            var md = new Markdown(new MarkdownOptions {AutoHyperlink = true, AutoNewLines = true});
-            return md.Transform(template);
-        }
-    }
+using System;
+using System.IO;
+using System.Text;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.App.Modules.Messaging.Templating.Formatting;
+using  ColorCode;
+
+namespace Coderr.Server.App.Modules.Messaging.Templating
+{
+    /// 
+    ///     Used to parse texts (convert markdown and extras to HTML)
+    /// 
+    [ContainerService(IsTransient = true)]
+    public class TemplateParser : ITemplateParser
+    {
+        /// 
+        ///     Run all formatters (replace text tokens, run markdown and finalize with a syntax highlighter)
+        /// 
+        /// template to parse
+        /// viewModel used to fill the template.
+        /// HTML
+        public string RunAll(Template messageTemplate, object viewModel)
+        {
+            if (messageTemplate == null) throw new ArgumentNullException("messageTemplate");
+
+            var sr = new StreamReader(messageTemplate.Content);
+            var text = sr.ReadToEnd();
+            var formatter = new StringFormatter();
+            text = formatter.Format(text, viewModel);
+            text = Markdown(text);
+            return ColorizeCode(text);
+        }
+
+        /// 
+        ///     Run only the viewModel replacer.
+        /// 
+        /// template to parse
+        /// viewModel used to fill the template.
+        /// HTML
+        public string RunFormatterOnly(Template messageTemplate, object viewModel)
+        {
+            if (messageTemplate == null) throw new ArgumentNullException("messageTemplate");
+
+            var sr = new StreamReader(messageTemplate.Content);
+            var text = sr.ReadToEnd();
+            //return Smart.Format(text, viewModel);
+            var formatter = new StringFormatter();
+            return formatter.Format(text, viewModel);
+        }
+
+        private static string ColorizeCode(string html)
+        {
+            var colorizer = new CodeColorizer();
+            var sb = new StringBuilder();
+            var pos = html.IndexOf("
", StringComparison.Ordinal);
+            if (pos > -1)
+            {
+                var lastPos = 0;
+                sb.Append(html.Substring(0, pos));
+                while (pos > -1)
+                {
+                    lastPos = html.IndexOf("
", pos, StringComparison.Ordinal); + + var snippet = html.Substring(pos + 11, lastPos - pos - 11); + var colorizedSnippet = colorizer.Colorize(snippet, Languages.CSharp); + sb.Append(colorizedSnippet); + + pos = html.IndexOf("
", lastPos, StringComparison.Ordinal);
+                }
+
+                sb.Append(html.Substring(lastPos + 13));
+                html = sb.ToString();
+            }
+
+            return html;
+        }
+
+        private static string Markdown(string template)
+        {
+            return Markdig.Markdown.ToHtml(template);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Mine/IRecommendationProvider.cs b/src/Server/Coderr.Server.App/Modules/Mine/IRecommendationProvider.cs
new file mode 100644
index 00000000..00946ffb
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Mine/IRecommendationProvider.cs
@@ -0,0 +1,29 @@
+using System.Threading.Tasks;
+
+namespace Coderr.Server.App.Modules.Mine
+{
+    /// 
+    ///     A provider that will suggest incidents
+    /// 
+    /// 
+    ///     
+    ///         A provider can recommend several incident. If they do, it's recommended that each incident get a different
+    ///         score.
+    ///         If all providers are equally worth, they can give a score of 100 to the most recommended incident. However,
+    ///         some providers affect the business more (like partitions) and could therefore specify a score multiplier for
+    ///         all suggested incidents.
+    ///     
+    ///     
+    ///         The provider will be executed in a container scope.
+    ///     
+    /// 
+    public interface IRecommendationProvider
+    {
+        /// 
+        ///     Suggest an incident.
+        /// 
+        /// Context information
+        /// task
+        Task Recommend(RecommendIncidentContext context);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Mine/IRecommendationService.cs b/src/Server/Coderr.Server.App/Modules/Mine/IRecommendationService.cs
new file mode 100644
index 00000000..929722a9
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Mine/IRecommendationService.cs
@@ -0,0 +1,13 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.App.Modules.Mine
+{
+    /// 
+    /// Will gather all recommendations and sort them by the number of points.
+    /// 
+    public interface IRecommendationService
+    {
+        Task> GetRecommendations(int accountId, int? applicationId = null);
+    }
+}
diff --git a/src/Server/Coderr.Server.App/Modules/Mine/RecommendIncidentContext.cs b/src/Server/Coderr.Server.App/Modules/Mine/RecommendIncidentContext.cs
new file mode 100644
index 00000000..659fa78c
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Mine/RecommendIncidentContext.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coderr.Server.App.Modules.Mine
+{
+    /// 
+    ///     Context for 
+    /// 
+    public class RecommendIncidentContext
+    {
+        private readonly List _items;
+
+        public RecommendIncidentContext(List items)
+        {
+            _items = items ?? throw new ArgumentNullException(nameof(items));
+        }
+
+        /// 
+        ///     User that want a suggestion
+        /// 
+        public int AccountId { get; set; }
+
+        /// 
+        ///     If the user have selected a specific application
+        /// 
+        public int? ApplicationId { get; set; }
+
+        /// 
+        /// Number of items each provider should suggest.
+        /// 
+        public int NumberOfItems { get; set; } = 10;
+
+        /// 
+        ///     Add another item.
+        /// 
+        /// 
+        /// Used by providers that are worth more. 1 = equally worth. Corresponds to "Weight" for partitions. At most 10.
+        public void Add(RecommendedIncident suggestion, double scoreMultiplier)
+        {
+            if (suggestion == null) throw new ArgumentNullException(nameof(suggestion));
+
+            // Should only suggest the same incident once.
+            // thus if there are multiple suggestions for the same incident, increase the importance
+            // and include both motivations.
+            var first = _items.FirstOrDefault(x => x.IncidentId == suggestion.IncidentId);
+            if (first != null)
+            {
+                first.Score += (int)(suggestion.Score * scoreMultiplier);
+                first.Motivation += "\r\n" + suggestion.Motivation;
+                return;
+            }
+
+            _items.Add(suggestion);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Mine/RecommendationService.cs b/src/Server/Coderr.Server.App/Modules/Mine/RecommendationService.cs
new file mode 100644
index 00000000..8fe2a94e
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Mine/RecommendationService.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using log4net;
+
+namespace Coderr.Server.App.Modules.Mine
+{
+    [ContainerService]
+    public class RecommendationService : IRecommendationService
+    {
+        private readonly IRecommendationProvider[] _providers;
+        private ILog _logger = LogManager.GetLogger(typeof(RecommendationService));
+
+        public RecommendationService(IEnumerable providers)
+        {
+            _providers = providers.ToArray();
+        }
+
+        public async Task> GetRecommendations(int accountId, int? applicationId = null)
+        {
+            var items = new List();
+            var context = new RecommendIncidentContext(items) { AccountId = accountId, ApplicationId = applicationId };
+            foreach (var provider in _providers)
+            {
+                try
+                {
+                    await provider.Recommend(context);
+                }
+                catch (Exception ex)
+                {
+                    _logger.Error("Provider " + provider.GetType().FullName + " failed.", ex);
+                }
+
+            }
+
+            return items.OrderByDescending(x => x.Score).Take(10).ToList();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Mine/RecommendedIncident.cs b/src/Server/Coderr.Server.App/Modules/Mine/RecommendedIncident.cs
new file mode 100644
index 00000000..eeac88a5
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Mine/RecommendedIncident.cs
@@ -0,0 +1,51 @@
+namespace Coderr.Server.App.Modules.Mine
+{
+    /// 
+    ///     A suggestion
+    /// 
+    /// 
+    ///     
+    ///         The suggested incident service will enrich the information with incident details.
+    ///     
+    /// 
+    public class RecommendedIncident
+    {
+        /// 
+        ///     Application that the incident belongs to
+        /// 
+        /// 
+        ///     
+        ///         Will be enriched by the suggestion service if left 0
+        ///     
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Name of the application
+        /// 
+        /// 
+        ///     
+        ///         Will be enriched by the suggestion service if left 0
+        ///     
+        /// 
+        public string ApplicationName { get; set; }
+
+        /// 
+        ///     Suggested incident
+        /// 
+        public int IncidentId { get; set; }
+
+        /// 
+        ///     Why this item was suggested.
+        /// 
+        public string Motivation { get; set; }
+
+        /// 
+        ///     Calculated score.
+        /// 
+        /// 
+        ///     100 points should be distributed between all incidents that a provider recommends.
+        /// 
+        public int Score { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/MonthlyStats/AppResult.cs b/src/Server/Coderr.Server.App/Modules/MonthlyStats/AppResult.cs
new file mode 100644
index 00000000..76b76933
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/MonthlyStats/AppResult.cs
@@ -0,0 +1,8 @@
+namespace Coderr.Server.App.Modules.MonthlyStats
+{
+    public class AppResult
+    {
+        public int ApplicationId { get; set; }
+        public int Count { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/MonthlyStats/ApplicationUsageStatisticsDto.cs b/src/Server/Coderr.Server.App/Modules/MonthlyStats/ApplicationUsageStatisticsDto.cs
new file mode 100644
index 00000000..a2f20b54
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/MonthlyStats/ApplicationUsageStatisticsDto.cs
@@ -0,0 +1,15 @@
+namespace Coderr.Server.App.Modules.MonthlyStats
+{
+    public class ApplicationUsageStatisticsDto
+    {
+        public int ApplicationId { get; set; }
+        public int ReportCount { get; set; }
+        public int IncidentCount { get; set; }
+        public int ReOpenedCount { get; set; }
+        public int ClosedCount { get; set; }
+        public int IgnoredCount { get; set; }
+        public decimal? NumberOfDevelopers { get; set; }
+        public int? EstimatedNumberOfErrors { get; set; }
+        public bool IsEmpty => ReportCount == 0 && ClosedCount == 0;
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/MonthlyStats/CollectStatsJob.cs b/src/Server/Coderr.Server.App/Modules/MonthlyStats/CollectStatsJob.cs
new file mode 100644
index 00000000..b7ecae87
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/MonthlyStats/CollectStatsJob.cs
@@ -0,0 +1,301 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Infrastructure.Configuration;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.App.Modules.MonthlyStats
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class CollectStatsJob : IBackgroundJobAsync
+    {
+        private readonly IConfiguration _reportConfiguration;
+        private readonly IConfiguration _config;
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private static DateTime _reportDate = DateTime.MinValue;
+
+        public CollectStatsJob(IAdoNetUnitOfWork unitOfWork, IConfiguration config,
+            IConfiguration reportConfiguration)
+        {
+            _unitOfWork = unitOfWork;
+            _config = config;
+            _reportConfiguration = reportConfiguration;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            var lastMonthDate = DateTime.Today.AddMonths(-1);
+            if (_reportDate == lastMonthDate)
+                return;
+
+            var lastMonth = new DateTime(lastMonthDate.Year, lastMonthDate.Month, 1);
+            if (_config.Value.LatestUploadedMonth == null)
+            {
+                await ReportAllFoundMonths(lastMonth);
+                return;
+            }
+
+            if (_config.Value?.LatestUploadedMonth == lastMonth)
+                return;
+
+
+            await ReportMonth(lastMonth);
+        }
+
+        private async Task GetIncidentCounts(DateTime lastMonth)
+        {
+            var results = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"select Incidents.ApplicationId, count(incidents.Id)
+                                    from incidents
+                                    where Incidents.CreatedAtUtc >= @fromDate AND Incidents.CreatedAtUtc < @toDate
+                                    group by Incidents.ApplicationId";
+                cmd.AddParameter("fromDate", lastMonth);
+                cmd.AddParameter("toDate", lastMonth.AddMonths(1));
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new AppResult
+                        {
+                            ApplicationId = reader.GetInt32(0),
+                            Count = reader.GetInt32(1),
+
+                        };
+                        results.Add(item);
+                    }
+                }
+            }
+
+            return results.ToArray();
+        }
+
+        private async Task GetClosedCount(DateTime lastMonth)
+        {
+            var results = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"SELECT Incidents.ApplicationId, count(*)
+                                    FROM IncidentHistory
+                                    JOIN Incidents on (Incidents.Id = IncidentId)
+                                    WHERE IncidentHistory.state = 3
+                                    AND IncidentHistory.CreatedAtUtc >= @fromDate AND IncidentHistory.CreatedAtUtc < @toDate
+                                    GROUP BY Incidents.ApplicationId";
+                cmd.AddParameter("fromDate", lastMonth);
+                cmd.AddParameter("toDate", lastMonth.AddMonths(1));
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new AppResult
+                        {
+                            ApplicationId = reader.GetInt32(0),
+                            Count = reader.GetInt32(1)
+                        };
+                        results.Add(item);
+                    }
+                }
+            }
+
+            return results.ToArray();
+        }
+
+        private async Task GetReOpened(DateTime lastMonth)
+        {
+            var results = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"SELECT Incidents.ApplicationId, count(*)
+                                    FROM IncidentHistory
+                                    JOIN Incidents on (Incidents.Id = IncidentId)
+                                    WHERE IncidentHistory.state = 4
+                                    AND IncidentHistory.CreatedAtUtc >= @fromDate AND IncidentHistory.CreatedAtUtc < @toDate
+                                    GROUP BY Incidents.ApplicationId";
+                cmd.AddParameter("fromDate", lastMonth);
+                cmd.AddParameter("toDate", lastMonth.AddMonths(1));
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new AppResult
+                        {
+                            ApplicationId = reader.GetInt32(0),
+                            Count = reader.GetInt32(1)
+                        };
+                        results.Add(item);
+                    }
+                }
+            }
+
+            return results.ToArray();
+        }
+
+        private async Task GetIgnoredCount(DateTime lastMonth)
+        {
+            var results = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"SELECT Incidents.ApplicationId, count(*)
+                                    FROM IncidentHistory
+                                    JOIN Incidents on (Incidents.Id = IncidentId)
+                                    WHERE IncidentHistory.state = 2
+                                    AND IncidentHistory.CreatedAtUtc >= @fromDate AND IncidentHistory.CreatedAtUtc < @toDate
+                                    GROUP BY Incidents.ApplicationId";
+                cmd.AddParameter("fromDate", lastMonth);
+                cmd.AddParameter("toDate", lastMonth.AddMonths(1));
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new AppResult
+                        {
+                            ApplicationId = reader.GetInt32(0),
+                            Count = reader.GetInt32(1)
+                        };
+                        results.Add(item);
+                    }
+                }
+            }
+
+            return results.ToArray();
+        }
+
+        private async Task GetReportCounts(DateTime lastMonth)
+        {
+            var results = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"SELECT ApplicationId, count(*)
+                                    FROM IncidentReports
+                                    JOIN Incidents ON (Incidents.Id = IncidentId)
+                                    where IncidentReports.ReceivedAtUtc >= @fromDate 
+                                        AND IncidentReports.ReceivedAtUtc < @toDate
+                                    group by ApplicationId";
+                cmd.AddParameter("fromDate", lastMonth);
+                cmd.AddParameter("toDate", lastMonth.AddMonths(1));
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new AppResult
+                        {
+                            ApplicationId = reader.GetInt32(0),
+                            Count = reader.GetInt32(1)
+                        };
+                        results.Add(item);
+                    }
+                }
+            }
+
+            return results.ToArray();
+        }
+
+        private async Task ReportAllFoundMonths(DateTime lastMonth)
+        {
+            while (true)
+            {
+                var result = await ReportMonth(lastMonth);
+                if (!result)
+                    break;
+
+                lastMonth = lastMonth.AddMonths(-1);
+            }
+        }
+
+        private async Task ReportMonth(DateTime lastMonth)
+        {
+            var apps = new Dictionary();
+
+            await GetApps(apps);
+
+            var values = await GetIncidentCounts(lastMonth);
+            MergeStats(values, apps, (stat, value) => stat.IncidentCount = value);
+
+            values = await GetReportCounts(lastMonth);
+            MergeStats(values, apps, (stat, value) => stat.ReportCount = value);
+
+            values = await GetClosedCount(lastMonth);
+            MergeStats(values, apps, (stat, value) => stat.ClosedCount = value);
+
+            values = await GetReOpened(lastMonth);
+            MergeStats(values, apps, (stat, value) => stat.ReOpenedCount = value);
+
+            values = await GetIgnoredCount(lastMonth);
+            MergeStats(values, apps, (stat, value) => stat.IgnoredCount = value);
+
+            if (_config.Value.LatestUploadedMonth == null || _config.Value.LatestUploadedMonth < lastMonth)
+            {
+                _config.Value.LatestUploadedMonth = lastMonth;
+                _reportDate = lastMonth;
+                _config.Save();
+            }
+
+            var allIsEmpty = apps.All(x => x.Value.IsEmpty);
+            if (allIsEmpty)
+                return false;
+
+            var dto = new UsageStatisticsDto
+            {
+                InstallationId = _reportConfiguration.Value.InstallationId,
+                Applications = apps.Values.ToArray(),
+                YearMonth = lastMonth
+            };
+            var json = JsonConvert.SerializeObject(dto);
+            var client = new HttpClient();
+            var content = new StringContent(json, Encoding.UTF8, "application/json");
+            //await client.PostAsync("https://coderr.io/stats/usage", content);
+            return true;
+        }
+
+        private async Task GetApps(Dictionary apps)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"select Id, EstimatedNumberOfErrors, NumberOfFtes FROM Applications";
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var estimatedNumberOfErrors = reader[1];
+                        var numberOfDevelopers = reader[2];
+                        var item = new ApplicationUsageStatisticsDto()
+                        {
+                            ApplicationId = reader.GetInt32(0),
+                            EstimatedNumberOfErrors =
+                                estimatedNumberOfErrors is DBNull ? 0 : (int) estimatedNumberOfErrors,
+                            NumberOfDevelopers = numberOfDevelopers is DBNull ? 0 : (decimal) numberOfDevelopers
+                        };
+                        apps[item.ApplicationId] = item;
+                    }
+                }
+            }
+        }
+
+        private static void MergeStats(IEnumerable appResults, IDictionary apps, Action assignMethod)
+        {
+            foreach (var count in appResults)
+            {
+                if (count.Count == 0)
+                    continue;
+                if (!apps.TryGetValue(count.ApplicationId, out var value))
+                {
+                    value = new ApplicationUsageStatisticsDto
+                    {
+                        ApplicationId = count.ApplicationId
+                    };
+                    apps[value.ApplicationId] = value;
+                }
+
+                assignMethod(value, count.Count);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/MonthlyStats/UsageStatisticsDto.cs b/src/Server/Coderr.Server.App/Modules/MonthlyStats/UsageStatisticsDto.cs
new file mode 100644
index 00000000..a8afd20f
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/MonthlyStats/UsageStatisticsDto.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Coderr.Server.App.Modules.MonthlyStats
+{
+    public class UsageStatisticsDto
+    {
+        public string InstallationId { get; set; }
+        public ApplicationUsageStatisticsDto[] Applications { get; set; }
+
+        public DateTime YearMonth { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/MonthlyStats/UsageStatsSettings.cs b/src/Server/Coderr.Server.App/Modules/MonthlyStats/UsageStatsSettings.cs
new file mode 100644
index 00000000..4a6cccb2
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/MonthlyStats/UsageStatsSettings.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using Coderr.Server.Abstractions.Config;
+
+namespace Coderr.Server.App.Modules.MonthlyStats
+{
+    public class UsageStatsSettings : IConfigurationSection
+    {
+        public DateTime? LatestUploadedMonth { get; set; }
+
+        string IConfigurationSection.SectionName => "UsageStats";
+
+        void IConfigurationSection.Load(IDictionary settings)
+        {
+            if (settings.Count == 0)
+                return;
+
+            var value = settings["LatestUploadedMonth"];
+            LatestUploadedMonth = DateTime.Parse(value).ToUniversalTime();
+        }
+
+        IDictionary IConfigurationSection.ToDictionary()
+        {
+            if (LatestUploadedMonth == null)
+                return new Dictionary();
+
+            var value = LatestUploadedMonth.Value.ToString("R", CultureInfo.InvariantCulture);
+            return new Dictionary
+            {
+                {"LatestUploadedMonth", value}
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Onboarding/Commands/SetOnboardingChoicesHandler.cs b/src/Server/Coderr.Server.App/Modules/Onboarding/Commands/SetOnboardingChoicesHandler.cs
new file mode 100644
index 00000000..d780671a
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Onboarding/Commands/SetOnboardingChoicesHandler.cs
@@ -0,0 +1,36 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Api.Modules.Onboarding.Commands;
+using DotNetCqs;
+
+namespace Coderr.Server.App.Modules.Onboarding.Commands
+{
+    class SetOnboardingChoicesHandler  : IMessageHandler
+    {
+        private IConfiguration _settings;
+
+        public SetOnboardingChoicesHandler(IConfiguration settings)
+        {
+            _settings = settings;
+        }
+
+        public Task HandleAsync(IMessageContext context, SetOnboardingChoices message)
+        {
+            if (!string.IsNullOrEmpty(message.MainLanguage))
+            {
+                _settings.Value.MainLanguage = message.MainLanguage;
+            }
+
+            if (message.Libraries?.Any()==true)
+            {
+                _settings.Value.Libraries = message.Libraries;
+                _settings.Value.IsComplete = true;
+            }
+
+            _settings.Save();
+
+            return Task.CompletedTask;
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.App/Modules/Onboarding/OnboardingSettings.cs b/src/Server/Coderr.Server.App/Modules/Onboarding/OnboardingSettings.cs
new file mode 100644
index 00000000..250d3285
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Onboarding/OnboardingSettings.cs
@@ -0,0 +1,45 @@
+
+using System.Collections.Generic;
+using Coderr.Server.Abstractions.Config;
+
+namespace Coderr.Server.App.Modules.Onboarding
+{
+    /// 
+    /// Current state of onboarding.
+    /// 
+    public class OnboardingSettings : IConfigurationSection
+    {
+        public bool IsComplete { get; set; }
+        public IReadOnlyList Libraries { get; set; }
+
+
+        /// 
+        ///     DOTNET or NODEJS
+        /// 
+        public string MainLanguage { get; set; }
+
+        public string SectionName { get; } = "Onboarding";
+
+        public string Feedback { get; set; }
+
+        public void Load(IDictionary settings)
+        {
+            if (settings.TryGetValue("MainLanguage", out var lang)) MainLanguage = lang;
+            if (settings.TryGetValue("Feedback", out var value)) Feedback = value;
+            if (settings.TryGetValue("IsComplete", out var complete)) IsComplete = complete == true.ToString();
+            if (settings.TryGetValue("Libraries", out var libs)) Libraries = libs.Split(',');
+        }
+
+        public IDictionary ToDictionary()
+        {
+            var items = new Dictionary
+            {
+                ["IsComplete"] = IsComplete.ToString(),
+                [nameof(MainLanguage)] = MainLanguage,
+                [nameof(Libraries)] = Libraries == null ? "" : string.Join(",", Libraries),
+                [nameof(Feedback)] = Feedback
+            };
+            return items;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Onboarding/Queries/GetOnboardingStateHandler.cs b/src/Server/Coderr.Server.App/Modules/Onboarding/Queries/GetOnboardingStateHandler.cs
new file mode 100644
index 00000000..4c404bc6
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Onboarding/Queries/GetOnboardingStateHandler.cs
@@ -0,0 +1,27 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Api.Modules.Onboarding.Queries;
+using DotNetCqs;
+
+namespace Coderr.Server.App.Modules.Onboarding.Queries
+{
+    internal class GetOnboardingStateHandler : IQueryHandler
+    {
+        private readonly IConfiguration _settings;
+
+        public GetOnboardingStateHandler(IConfiguration settings)
+        {
+            _settings = settings;
+        }
+
+        public Task HandleAsync(IMessageContext context, GetOnboardingState query)
+        {
+            return Task.FromResult(new GetOnboardingStateResult
+            {
+                IsComplete = _settings.Value.IsComplete,
+                Libraries = _settings.Value.Libraries,
+                MainLanguage = _settings.Value.MainLanguage
+            });
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsForApplicationHandler.cs b/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsForApplicationHandler.cs
new file mode 100644
index 00000000..0d4d5242
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsForApplicationHandler.cs
@@ -0,0 +1,29 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Tagging;
+using Coderr.Server.Api.Modules.Tagging.Queries;
+using Coderr.Server.Domain.Modules.Tags;
+using DotNetCqs;
+
+namespace Coderr.Server.App.Modules.Tagging.Handlers
+{
+    internal class GetTagsForApplicationHandler : IQueryHandler
+    {
+        private readonly ITagsRepository _repository;
+
+        public GetTagsForApplicationHandler(ITagsRepository repository)
+        {
+            _repository = repository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetTagsForApplication query)
+        {
+            return (await _repository.GetApplicationTagsAsync(query.ApplicationId)).Select(ConvertTag).ToArray();
+        }
+
+        private TagDTO ConvertTag(Tag arg)
+        {
+            return new TagDTO {Name = arg.Name, OrderNumber = arg.OrderNumber};
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsForIncidentHandler.cs b/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsForIncidentHandler.cs
new file mode 100644
index 00000000..4d4605b9
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsForIncidentHandler.cs
@@ -0,0 +1,30 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Tagging;
+using Coderr.Server.Api.Modules.Tagging.Queries;
+using Coderr.Server.Domain.Modules.Tags;
+using DotNetCqs;
+
+
+namespace Coderr.Server.App.Modules.Tagging.Handlers
+{
+    internal class GetTagsForIncidentHandler : IQueryHandler
+    {
+        private readonly ITagsRepository _repository;
+
+        public GetTagsForIncidentHandler(ITagsRepository repository)
+        {
+            _repository = repository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetTagsForIncident query)
+        {
+            return (await _repository.GetIncidentTagsAsync(query.IncidentId)).Select(ConvertTag).ToArray();
+        }
+
+        private TagDTO ConvertTag(Tag arg)
+        {
+            return new TagDTO {Name = arg.Name, OrderNumber = arg.OrderNumber};
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsHandler.cs b/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsHandler.cs
new file mode 100644
index 00000000..de57b55b
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Tagging/Handlers/GetTagsHandler.cs
@@ -0,0 +1,29 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Tagging;
+using Coderr.Server.Api.Modules.Tagging.Queries;
+using Coderr.Server.Domain.Modules.Tags;
+using DotNetCqs;
+
+namespace Coderr.Server.App.Modules.Tagging.Handlers
+{
+    internal class GetTagsHandler : IQueryHandler
+    {
+        private readonly ITagsRepository _repository;
+
+        public GetTagsHandler(ITagsRepository repository)
+        {
+            _repository = repository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetTags query)
+        {
+            return (await _repository.GetTagsAsync(query.ApplicationId, query.IncidentId)).Select(ConvertTag).ToArray();
+        }
+
+        private TagDTO ConvertTag(Tag arg)
+        {
+            return new TagDTO {Name = arg.Name, OrderNumber = arg.OrderNumber};
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/ActionConfigurationData.cs b/src/Server/Coderr.Server.App/Modules/Triggers/ActionConfigurationData.cs
new file mode 100644
index 00000000..01cfc47a
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/ActionConfigurationData.cs
@@ -0,0 +1,28 @@
+namespace Coderr.Server.App.Modules.Triggers
+{
+    /// 
+    ///     Defines information for a specific action in a trigger.
+    /// 
+    /// 
+    ///     
+    ///         "Send email", for instance, might have email address as .
+    ///     
+    /// 
+    public class ActionConfigurationData
+    {
+        /// 
+        ///     Action to take
+        /// 
+        public string ActionName { get; set; }
+
+        /// 
+        ///     Context data for the action.
+        /// 
+        public string Data { get; set; }
+
+        /// 
+        ///     Primary key
+        /// 
+        public int Id { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Commands/CreateTriggerHandler.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Commands/CreateTriggerHandler.cs
similarity index 82%
rename from src/Server/OneTrueError.App/Modules/Triggers/Commands/CreateTriggerHandler.cs
rename to src/Server/Coderr.Server.App/Modules/Triggers/Commands/CreateTriggerHandler.cs
index 3329d63b..ba486e26 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Commands/CreateTriggerHandler.cs
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Commands/CreateTriggerHandler.cs
@@ -1,61 +1,59 @@
-using System;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using OneTrueError.Api.Modules.Triggers.Commands;
-using OneTrueError.App.Modules.Triggers.Domain;
-
-namespace OneTrueError.App.Modules.Triggers.Commands
-{
-    /// 
-    ///     Handler for .
-    /// 
-    [Component]
-    public class CreateTriggerHandler : ICommandHandler
-    {
-        private readonly ITriggerRepository _repository;
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// repos
-        public CreateTriggerHandler(ITriggerRepository repository)
-        {
-            if (repository == null) throw new ArgumentNullException("repository");
-            _repository = repository;
-        }
-
-
-        /// 
-        ///     Execute a command asynchronously.
-        /// 
-        /// Command to execute.
-        /// 
-        ///     Task which will be completed once the command has been executed.
-        /// 
-        public async Task ExecuteAsync(CreateTrigger command)
-        {
-            var domainModel = new Trigger(command.ApplicationId)
-            {
-                Description = command.Description,
-                LastTriggerAction = DtoToDomainConverters.ConvertLastAction(command.LastTriggerAction),
-                Id = command.Id,
-                Name = command.Name,
-                RunForExistingIncidents = command.RunForExistingIncidents,
-                RunForReopenedIncidents = command.RunForReOpenedIncidents,
-                RunForNewIncidents = command.RunForNewIncidents
-            };
-
-            foreach (var action in command.Actions)
-            {
-                domainModel.AddAction(DtoToDomainConverters.ConvertAction(action));
-            }
-            foreach (var rule in command.Rules)
-            {
-                domainModel.AddRule(DtoToDomainConverters.ConvertRule(rule));
-            }
-
-            await _repository.CreateAsync(domainModel);
-        }
-    }
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Triggers.Commands;
+using DotNetCqs;
+
+
+namespace Coderr.Server.App.Modules.Triggers.Commands
+{
+    /// 
+    ///     Handler for .
+    /// 
+    public class CreateTriggerHandler : IMessageHandler
+    {
+        private readonly ITriggerRepository _repository;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repos
+        public CreateTriggerHandler(ITriggerRepository repository)
+        {
+            if (repository == null) throw new ArgumentNullException("repository");
+            _repository = repository;
+        }
+
+
+        /// 
+        ///     Execute a command asynchronously.
+        /// 
+        /// Command to execute.
+        /// 
+        ///     Task which will be completed once the command has been executed.
+        /// 
+        public async Task HandleAsync(IMessageContext context, CreateTrigger command)
+        {
+            var domainModel = new Trigger(command.ApplicationId)
+            {
+                Description = command.Description,
+                LastTriggerAction = DtoToDomainConverters.ConvertLastAction(command.LastTriggerAction),
+                Id = command.Id,
+                Name = command.Name,
+                RunForExistingIncidents = command.RunForExistingIncidents,
+                RunForReopenedIncidents = command.RunForReOpenedIncidents,
+                RunForNewIncidents = command.RunForNewIncidents
+            };
+
+            foreach (var action in command.Actions)
+            {
+                domainModel.AddAction(DtoToDomainConverters.ConvertAction(action));
+            }
+            foreach (var rule in command.Rules)
+            {
+                domainModel.AddRule(DtoToDomainConverters.ConvertRule(rule));
+            }
+
+            await _repository.CreateAsync(domainModel);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Commands/DtoToDomainConverters.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Commands/DtoToDomainConverters.cs
similarity index 84%
rename from src/Server/OneTrueError.App/Modules/Triggers/Commands/DtoToDomainConverters.cs
rename to src/Server/Coderr.Server.App/Modules/Triggers/Commands/DtoToDomainConverters.cs
index 65c79524..1b189ed4 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Commands/DtoToDomainConverters.cs
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Commands/DtoToDomainConverters.cs
@@ -1,141 +1,140 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using OneTrueError.Api.Modules.Triggers;
-using OneTrueError.App.Modules.Triggers.Domain;
-using OneTrueError.App.Modules.Triggers.Domain.Rules;
-
-namespace OneTrueError.App.Modules.Triggers.Commands
-{
-    /// 
-    ///     Convert DTOs to Entities.
-    /// 
-    /// 
-    ///     
-    ///         Unknown enum values and unknown sub classes should generate exceptions (FormatException) so that we know
-    ///         that the converts have not been updated.
-    ///     
-    /// 
-    public static class DtoToDomainConverters
-    {
-        /// 
-        ///     Convert the dto to an entity
-        /// 
-        /// dto
-        /// entity
-        public static ActionConfigurationData ConvertAction(TriggerActionDataDTO action)
-        {
-            if (action == null) throw new ArgumentNullException("action");
-            return new ActionConfigurationData
-            {
-                Data = action.ActionContext,
-                ActionName = action.ActionName
-            };
-        }
-
-        /// 
-        ///     Convert the dto to an entity
-        /// 
-        /// dto
-        /// entity
-        /// Unknown enum value in entity
-        public static FilterCondition ConvertFilterCondition(TriggerFilterCondition filter)
-        {
-            switch (filter)
-            {
-                case TriggerFilterCondition.Contains:
-                    return FilterCondition.Contains;
-                case TriggerFilterCondition.DoNotContain:
-                    return FilterCondition.DoNotContain;
-                case TriggerFilterCondition.EndsWith:
-                    return FilterCondition.EndsWith;
-                case TriggerFilterCondition.Equals:
-                    return FilterCondition.Equals;
-                case TriggerFilterCondition.StartsWith:
-                    return FilterCondition.StartsWith;
-                default:
-                    throw new FormatException(string.Format("Value '{0}' do not exist in the {1} enum.",
-                        filter, typeof(FilterCondition).Name));
-            }
-        }
-
-
-        /// 
-        ///     Convert filter
-        /// 
-        /// dto
-        /// entity
-        /// Entity enum contains a value that is currently not handled.
-        [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterResult")]
-        public static FilterResult ConvertFilterResult(TriggerRuleAction ruleAction)
-        {
-            switch (ruleAction)
-            {
-                case TriggerRuleAction.AbortTrigger:
-                    return FilterResult.Revoke;
-                case TriggerRuleAction.ContinueWithNextRule:
-                    return FilterResult.Continue;
-                case TriggerRuleAction.ExecuteActions:
-                    return FilterResult.Grant;
-                default:
-                    throw new FormatException(string.Format("Value '{0}' do not exist in the {1} enum.",
-                        ruleAction, typeof(FilterResult).Name));
-            }
-        }
-
-        /// 
-        ///     Convert last action
-        /// 
-        /// dto
-        /// entity
-        /// Entity enum value is not recognized.
-        public static LastTriggerAction ConvertLastAction(LastTriggerActionDTO lastTriggerAction)
-        {
-            switch (lastTriggerAction)
-            {
-                case LastTriggerActionDTO.AbortTrigger:
-                    return LastTriggerAction.Revoke;
-                case LastTriggerActionDTO.ExecuteActions:
-                    return LastTriggerAction.Grant;
-                default:
-                    throw new FormatException(string.Format("Value '{0}' do not exist in the {1} enum.",
-                        lastTriggerAction, typeof(LastTriggerAction).Name));
-            }
-        }
-
-        /// 
-        ///     Convert all different types of rules to entities
-        /// 
-        /// dto
-        /// entity
-        /// Subclass is not recognized.
-        public static ITriggerRule ConvertRule(TriggerRuleBase rule)
-        {
-            if (rule is TriggerContextRule)
-            {
-                var dto = (TriggerContextRule) rule;
-                return new ContextCollectionRule
-                {
-                    ContextName = dto.ContextName,
-                    Condition = ConvertFilterCondition(dto.Filter),
-                    PropertyName = dto.PropertyName,
-                    PropertyValue = dto.PropertyValue,
-                    ResultToUse = ConvertFilterResult(dto.ResultToUse)
-                };
-            }
-
-            if (rule is TriggerExceptionRule)
-            {
-                var dto = (TriggerExceptionRule) rule;
-                return new ExceptionRule
-                {
-                    FieldName = dto.FieldName,
-                    Condition = ConvertFilterCondition(dto.Filter),
-                    ResultToUse = ConvertFilterResult(dto.ResultToUse),
-                    Value = dto.Value
-                };
-            }
-
-            throw new FormatException("Failed to convert " + rule);
-        }
-    }
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Coderr.Server.Api.Modules.Triggers;
+using Coderr.Server.App.Modules.Triggers.Rules;
+
+namespace Coderr.Server.App.Modules.Triggers.Commands
+{
+    /// 
+    ///     Convert DTOs to Entities.
+    /// 
+    /// 
+    ///     
+    ///         Unknown enum values and unknown sub classes should generate exceptions (FormatException) so that we know
+    ///         that the converts have not been updated.
+    ///     
+    /// 
+    public static class DtoToDomainConverters
+    {
+        /// 
+        ///     Convert the dto to an entity
+        /// 
+        /// dto
+        /// entity
+        public static ActionConfigurationData ConvertAction(TriggerActionDataDTO action)
+        {
+            if (action == null) throw new ArgumentNullException("action");
+            return new ActionConfigurationData
+            {
+                Data = action.ActionContext,
+                ActionName = action.ActionName
+            };
+        }
+
+        /// 
+        ///     Convert the dto to an entity
+        /// 
+        /// dto
+        /// entity
+        /// Unknown enum value in entity
+        public static FilterCondition ConvertFilterCondition(TriggerFilterCondition filter)
+        {
+            switch (filter)
+            {
+                case TriggerFilterCondition.Contains:
+                    return FilterCondition.Contains;
+                case TriggerFilterCondition.DoNotContain:
+                    return FilterCondition.DoNotContain;
+                case TriggerFilterCondition.EndsWith:
+                    return FilterCondition.EndsWith;
+                case TriggerFilterCondition.Equals:
+                    return FilterCondition.Equals;
+                case TriggerFilterCondition.StartsWith:
+                    return FilterCondition.StartsWith;
+                default:
+                    throw new FormatException(string.Format((string) "Value '{0}' do not exist in the {1} enum.",
+                        (object) filter, typeof(FilterCondition).Name));
+            }
+        }
+
+
+        /// 
+        ///     Convert filter
+        /// 
+        /// dto
+        /// entity
+        /// Entity enum contains a value that is currently not handled.
+        [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterResult")]
+        public static FilterResult ConvertFilterResult(TriggerRuleAction ruleAction)
+        {
+            switch (ruleAction)
+            {
+                case TriggerRuleAction.AbortTrigger:
+                    return FilterResult.Revoke;
+                case TriggerRuleAction.ContinueWithNextRule:
+                    return FilterResult.Continue;
+                case TriggerRuleAction.ExecuteActions:
+                    return FilterResult.Grant;
+                default:
+                    throw new FormatException(string.Format((string) "Value '{0}' do not exist in the {1} enum.",
+                        (object) ruleAction, typeof(FilterResult).Name));
+            }
+        }
+
+        /// 
+        ///     Convert last action
+        /// 
+        /// dto
+        /// entity
+        /// Entity enum value is not recognized.
+        public static LastTriggerAction ConvertLastAction(LastTriggerActionDTO lastTriggerAction)
+        {
+            switch (lastTriggerAction)
+            {
+                case LastTriggerActionDTO.AbortTrigger:
+                    return LastTriggerAction.Revoke;
+                case LastTriggerActionDTO.ExecuteActions:
+                    return LastTriggerAction.Grant;
+                default:
+                    throw new FormatException(string.Format((string) "Value '{0}' do not exist in the {1} enum.",
+                        (object) lastTriggerAction, typeof(LastTriggerAction).Name));
+            }
+        }
+
+        /// 
+        ///     Convert all different types of rules to entities
+        /// 
+        /// dto
+        /// entity
+        /// Subclass is not recognized.
+        public static RuleBase ConvertRule(TriggerRuleBase rule)
+        {
+            if (rule is TriggerContextRule)
+            {
+                var dto = (TriggerContextRule) rule;
+                return new ContextCollectionRule
+                {
+                    ContextName = dto.ContextName,
+                    Condition = ConvertFilterCondition(dto.Filter),
+                    PropertyName = dto.PropertyName,
+                    PropertyValue = dto.PropertyValue,
+                    ResultToUse = ConvertFilterResult(dto.ResultToUse)
+                };
+            }
+
+            if (rule is TriggerExceptionRule)
+            {
+                var dto = (TriggerExceptionRule) rule;
+                return new ExceptionRule
+                {
+                    FieldName = dto.FieldName,
+                    Condition = ConvertFilterCondition(dto.Filter),
+                    ResultToUse = ConvertFilterResult(dto.ResultToUse),
+                    Value = dto.Value
+                };
+            }
+
+            throw new FormatException("Failed to convert " + rule);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Commands/UpdateTriggerHandler.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Commands/UpdateTriggerHandler.cs
similarity index 84%
rename from src/Server/OneTrueError.App/Modules/Triggers/Commands/UpdateTriggerHandler.cs
rename to src/Server/Coderr.Server.App/Modules/Triggers/Commands/UpdateTriggerHandler.cs
index 79bf4391..c4696654 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Commands/UpdateTriggerHandler.cs
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Commands/UpdateTriggerHandler.cs
@@ -1,63 +1,61 @@
-using System;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using OneTrueError.Api.Modules.Triggers.Commands;
-using OneTrueError.App.Modules.Triggers.Domain;
-
-namespace OneTrueError.App.Modules.Triggers.Commands
-{
-    /// 
-    ///     Handler for .
-    /// 
-    [Component]
-    public class UpdateTriggerHandler : ICommandHandler
-    {
-        private readonly ITriggerRepository _triggerRepository;
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// repos
-        /// triggerRepository
-        public UpdateTriggerHandler(ITriggerRepository triggerRepository)
-        {
-            if (triggerRepository == null) throw new ArgumentNullException("triggerRepository");
-            _triggerRepository = triggerRepository;
-        }
-
-
-        /// 
-        ///     Execute a command asynchronously.
-        /// 
-        /// Command to execute.
-        /// 
-        ///     Task which will be completed once the command has been executed.
-        /// 
-        public async Task ExecuteAsync(UpdateTrigger command)
-        {
-            if (command == null) throw new ArgumentNullException("command");
-            var domainEntity = await _triggerRepository.GetAsync(command.Id);
-            domainEntity.Description = command.Description;
-            domainEntity.LastTriggerAction = DtoToDomainConverters.ConvertLastAction(command.LastTriggerAction);
-            domainEntity.Name = command.Name;
-            domainEntity.RunForExistingIncidents = command.RunForExistingIncidents;
-            domainEntity.RunForReopenedIncidents = command.RunForReOpenedIncidents;
-            domainEntity.RunForNewIncidents = command.RunForNewIncidents;
-
-            domainEntity.RemoveActions();
-            foreach (var action in command.Actions)
-            {
-                domainEntity.AddAction(DtoToDomainConverters.ConvertAction(action));
-            }
-
-            domainEntity.RemoveRules();
-            foreach (var rule in command.Rules)
-            {
-                domainEntity.AddRule(DtoToDomainConverters.ConvertRule(rule));
-            }
-
-            await _triggerRepository.UpdateAsync(domainEntity);
-        }
-    }
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Triggers.Commands;
+using DotNetCqs;
+
+
+namespace Coderr.Server.App.Modules.Triggers.Commands
+{
+    /// 
+    ///     Handler for .
+    /// 
+    public class UpdateTriggerHandler : IMessageHandler
+    {
+        private readonly ITriggerRepository _triggerRepository;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repos
+        /// triggerRepository
+        public UpdateTriggerHandler(ITriggerRepository triggerRepository)
+        {
+            if (triggerRepository == null) throw new ArgumentNullException("triggerRepository");
+            _triggerRepository = triggerRepository;
+        }
+
+
+        /// 
+        ///     Execute a command asynchronously.
+        /// 
+        /// Command to execute.
+        /// 
+        ///     Task which will be completed once the command has been executed.
+        /// 
+        public async Task HandleAsync(IMessageContext context, UpdateTrigger command)
+        {
+            if (command == null) throw new ArgumentNullException("command");
+            var domainEntity = await _triggerRepository.GetAsync(command.Id);
+            domainEntity.Description = command.Description;
+            domainEntity.LastTriggerAction = DtoToDomainConverters.ConvertLastAction(command.LastTriggerAction);
+            domainEntity.Name = command.Name;
+            domainEntity.RunForExistingIncidents = command.RunForExistingIncidents;
+            domainEntity.RunForReopenedIncidents = command.RunForReOpenedIncidents;
+            domainEntity.RunForNewIncidents = command.RunForNewIncidents;
+
+            domainEntity.RemoveActions();
+            foreach (var action in command.Actions)
+            {
+                domainEntity.AddAction(DtoToDomainConverters.ConvertAction(action));
+            }
+
+            domainEntity.RemoveRules();
+            foreach (var rule in command.Rules)
+            {
+                domainEntity.AddRule(DtoToDomainConverters.ConvertRule(rule));
+            }
+
+            await _triggerRepository.UpdateAsync(domainEntity);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/DomainTriggerRuleJsonConverter.cs b/src/Server/Coderr.Server.App/Modules/Triggers/DomainTriggerRuleJsonConverter.cs
similarity index 84%
rename from src/Server/OneTrueError.App/Modules/Triggers/DomainTriggerRuleJsonConverter.cs
rename to src/Server/Coderr.Server.App/Modules/Triggers/DomainTriggerRuleJsonConverter.cs
index f73e5a6e..7a6372f9 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/DomainTriggerRuleJsonConverter.cs
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/DomainTriggerRuleJsonConverter.cs
@@ -1,55 +1,54 @@
-using System;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using OneTrueError.App.Modules.Triggers.Domain;
-using OneTrueError.App.Modules.Triggers.Domain.Rules;
-
-namespace OneTrueError.App.Modules.Triggers
-{
-    /// 
-    ///     Handles our rule inheritance in a more elegant way
-    /// 
-    public class DomainTriggerRuleJsonConverter : JsonCreationConverter
-    {
-        /// 
-        ///     Create an instance of objectType, based properties in the JSON object
-        /// 
-        /// type of object expected
-        /// 
-        ///     contents of JSON object that will be deserialized
-        /// 
-        /// 
-        protected override ITriggerRule Create(Type objectType, JObject jsonObject)
-        {
-            if (objectType == null) throw new ArgumentNullException("objectType");
-            if (jsonObject == null) throw new ArgumentNullException("jsonObject");
-            if (FieldExists("RuleType", jsonObject))
-            {
-                switch (jsonObject["RuleType"].ToString())
-                {
-                    case "Exception":
-                        return new ExceptionRule();
-                    case "ContextCollection":
-                        return new ContextCollectionRule();
-                }
-            }
-
-            if (jsonObject["ContextName"] != null)
-            {
-                return new ContextCollectionRule();
-            }
-
-            if (jsonObject["FieldName"] != null)
-            {
-                return new ExceptionRule();
-            }
-
-            throw new JsonReaderException("Unsupported rule: " + jsonObject);
-        }
-
-        private static bool FieldExists(string fieldName, JObject jObject)
-        {
-            return jObject[fieldName] != null;
-        }
-    }
+using System;
+using Coderr.Server.App.Modules.Triggers.Rules;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Coderr.Server.App.Modules.Triggers
+{
+    /// 
+    ///     Handles our rule inheritance in a more elegant way
+    /// 
+    public class DomainTriggerRuleJsonConverter : JsonCreationConverter
+    {
+        /// 
+        ///     Create an instance of objectType, based properties in the JSON object
+        /// 
+        /// type of object expected
+        /// 
+        ///     contents of JSON object that will be deserialized
+        /// 
+        /// 
+        protected override RuleBase Create(Type objectType, JObject jsonObject)
+        {
+            if (objectType == null) throw new ArgumentNullException("objectType");
+            if (jsonObject == null) throw new ArgumentNullException("jsonObject");
+            if (FieldExists("RuleType", jsonObject))
+            {
+                switch (jsonObject["RuleType"].ToString())
+                {
+                    case "Exception":
+                        return new ExceptionRule();
+                    case "ContextCollection":
+                        return new ContextCollectionRule();
+                }
+            }
+
+            if (jsonObject["ContextName"] != null)
+            {
+                return new ContextCollectionRule();
+            }
+
+            if (jsonObject["FieldName"] != null)
+            {
+                return new ExceptionRule();
+            }
+
+            throw new JsonReaderException("Unsupported rule: " + jsonObject);
+        }
+
+        private static bool FieldExists(string fieldName, JObject jObject)
+        {
+            return jObject[fieldName] != null;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/FilterCondition.cs b/src/Server/Coderr.Server.App/Modules/Triggers/FilterCondition.cs
new file mode 100644
index 00000000..a84cbbda
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/FilterCondition.cs
@@ -0,0 +1,35 @@
+using System.ComponentModel;
+
+namespace Coderr.Server.App.Modules.Triggers
+{
+    /// 
+    ///     Specifies how the filter value should be compared with the actual property data.
+    /// 
+    public enum FilterCondition
+    {
+        /// 
+        ///     Should start with the given value
+        /// 
+        [Description("Starts with")] StartsWith,
+
+        /// 
+        ///     Should end with the given value
+        /// 
+        [Description("Ends with")] EndsWith,
+
+        /// 
+        ///     Should contain the given value
+        /// 
+        [Description("Contain")] Contains,
+
+        /// 
+        ///     Should not contain the given value
+        /// 
+        [Description("Do not contain")] DoNotContain,
+
+        /// 
+        ///     Should equal the given value.
+        /// 
+        [Description("Equals")] Equals
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/FilterResult.cs b/src/Server/Coderr.Server.App/Modules/Triggers/FilterResult.cs
new file mode 100644
index 00000000..07f01b02
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/FilterResult.cs
@@ -0,0 +1,31 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.App.Modules.Triggers
+{
+    /// 
+    ///     Result for .
+    /// 
+    [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterResult")]
+    public enum FilterResult
+    {
+        /// 
+        ///     Rule did not match the given conditions.
+        /// 
+        NotMatched,
+
+        /// 
+        ///     Stop processing other rules and grant this report
+        /// 
+        Grant,
+
+        /// 
+        ///     Stop process other rules and revoke this report
+        /// 
+        Revoke,
+
+        /// 
+        ///     Ok by us, pass to any other roles.
+        /// 
+        Continue
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/ITriggerRepository.cs b/src/Server/Coderr.Server.App/Modules/Triggers/ITriggerRepository.cs
new file mode 100644
index 00000000..9b1cd5bc
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/ITriggerRepository.cs
@@ -0,0 +1,48 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Griffin.Data;
+
+namespace Coderr.Server.App.Modules.Triggers
+{
+    /// 
+    ///     Repository to load information for the Trigger root aggregate.
+    /// 
+    public interface ITriggerRepository
+    {
+        /// 
+        ///     Create a new trigger
+        /// 
+        /// trigger
+        /// task
+        Task CreateAsync(Trigger trigger);
+
+        /// 
+        ///     Delete a trigger
+        /// 
+        /// trigger PK
+        /// task
+        Task DeleteAsync(int id);
+
+        /// 
+        ///     Get a trigger
+        /// 
+        /// PK
+        /// trigger
+        /// Trigger was not found.
+        Task GetAsync(int id);
+
+        /// 
+        ///     Get all triggers for the given application
+        /// 
+        /// app PK
+        /// 
+        IEnumerable GetForApplication(int applicationId);
+
+        /// 
+        ///     Update trigger
+        /// 
+        /// trigger
+        /// task
+        Task UpdateAsync(Trigger entity);
+    }
+}
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/JsonCreationConverter.cs b/src/Server/Coderr.Server.App/Modules/Triggers/JsonCreationConverter.cs
similarity index 95%
rename from src/Server/OneTrueError.App/Modules/Triggers/JsonCreationConverter.cs
rename to src/Server/Coderr.Server.App/Modules/Triggers/JsonCreationConverter.cs
index 62887b51..27c031b3 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/JsonCreationConverter.cs
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/JsonCreationConverter.cs
@@ -1,71 +1,71 @@
-using System;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-
-namespace OneTrueError.App.Modules.Triggers
-{
-    /// 
-    ///     Base class for custom JSON serializers.
-    /// 
-    /// Type of entity
-    public abstract class JsonCreationConverter : JsonConverter
-    {
-        /// 
-        ///     Determines whether this instance can convert the specified object type.
-        /// 
-        /// Type of the object.
-        /// 
-        ///     true if this instance can convert the specified object type; otherwise, false.
-        /// 
-        public override bool CanConvert(Type objectType)
-        {
-            return typeof(T).IsAssignableFrom(objectType);
-        }
-
-        /// 
-        ///     Reads the JSON representation of the object.
-        /// 
-        /// The  to read from.
-        /// Type of the object.
-        /// The existing value of object being read.
-        /// The calling serializer.
-        /// 
-        ///     The object value.
-        /// 
-        public override object ReadJson(JsonReader reader,
-            Type objectType,
-            object existingValue,
-            JsonSerializer serializer)
-        {
-            if (serializer == null) throw new ArgumentNullException("serializer");
-
-            var jsonObject = JObject.Load(reader);
-            var target = Create(objectType, jsonObject);
-            serializer.Populate(jsonObject.CreateReader(), target);
-            return target;
-        }
-
-        /// 
-        ///     Writes the JSON representation of the object.
-        /// 
-        /// The  to write to.
-        /// The value.
-        /// The calling serializer.
-        public override void WriteJson(JsonWriter writer,
-            object value,
-            JsonSerializer serializer)
-        {
-            throw new NotSupportedException();
-        }
-
-        /// 
-        ///     Create an instance of objectType, based properties in the JSON object
-        /// 
-        /// type of object expected
-        /// 
-        ///     contents of JSON object that will be deserialized
-        /// 
-        /// 
-        protected abstract T Create(Type objectType, JObject jsonObject);
-    }
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Coderr.Server.App.Modules.Triggers
+{
+    /// 
+    ///     Base class for custom JSON serializers.
+    /// 
+    /// Type of entity
+    public abstract class JsonCreationConverter : JsonConverter
+    {
+        /// 
+        ///     Determines whether this instance can convert the specified object type.
+        /// 
+        /// Type of the object.
+        /// 
+        ///     true if this instance can convert the specified object type; otherwise, false.
+        /// 
+        public override bool CanConvert(Type objectType)
+        {
+            return typeof(T).IsAssignableFrom(objectType);
+        }
+
+        /// 
+        ///     Reads the JSON representation of the object.
+        /// 
+        /// The  to read from.
+        /// Type of the object.
+        /// The existing value of object being read.
+        /// The calling serializer.
+        /// 
+        ///     The object value.
+        /// 
+        public override object ReadJson(JsonReader reader,
+            Type objectType,
+            object existingValue,
+            JsonSerializer serializer)
+        {
+            if (serializer == null) throw new ArgumentNullException("serializer");
+
+            var jsonObject = JObject.Load(reader);
+            var target = Create(objectType, jsonObject);
+            serializer.Populate(jsonObject.CreateReader(), target);
+            return target;
+        }
+
+        /// 
+        ///     Writes the JSON representation of the object.
+        /// 
+        /// The  to write to.
+        /// The value.
+        /// The calling serializer.
+        public override void WriteJson(JsonWriter writer,
+            object value,
+            JsonSerializer serializer)
+        {
+            throw new NotSupportedException();
+        }
+
+        /// 
+        ///     Create an instance of objectType, based properties in the JSON object
+        /// 
+        /// type of object expected
+        /// 
+        ///     contents of JSON object that will be deserialized
+        /// 
+        /// 
+        protected abstract T Create(Type objectType, JObject jsonObject);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/LastTriggerAction.cs b/src/Server/Coderr.Server.App/Modules/Triggers/LastTriggerAction.cs
new file mode 100644
index 00000000..5498b732
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/LastTriggerAction.cs
@@ -0,0 +1,18 @@
+namespace Coderr.Server.App.Modules.Triggers
+{
+    /// 
+    ///     What to do if all filter rules have accepted the report.
+    /// 
+    public enum LastTriggerAction
+    {
+        /// 
+        ///     Grant actions execution.
+        /// 
+        Grant,
+
+        /// 
+        ///     Abort.
+        /// 
+        Revoke
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/Queries/DomainToDtoConverters.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Queries/DomainToDtoConverters.cs
new file mode 100644
index 00000000..5a0b7967
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Queries/DomainToDtoConverters.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using Coderr.Server.Api.Modules.Triggers;
+using Coderr.Server.App.Modules.Triggers.Rules;
+
+namespace Coderr.Server.App.Modules.Triggers.Queries
+{
+    /// 
+    ///     Converts triggers into DTOs which are transferred to the UI
+    /// 
+    /// 
+    ///     
+    ///         Unknown enum values and unknown sub classes should generate exceptions (FormatException) so that we know
+    ///         that the converts have not been updated.
+    ///     
+    /// 
+    public static class DomainToDtoConverters
+    {
+        /// 
+        ///     Convert action to a DTO
+        /// 
+        /// entity
+        /// DTO
+        public static TriggerActionDataDTO ConvertAction(ActionConfigurationData action)
+        {
+            if (action == null) throw new ArgumentNullException(nameof(action));
+            return new TriggerActionDataDTO
+            {
+                ActionContext = action.Data,
+                ActionName = action.ActionName
+            };
+        }
+
+        /// 
+        ///     Convert entity to DTO
+        /// 
+        /// entity
+        /// DTO
+        /// Unknown enum value in entity
+        public static TriggerFilterCondition ConvertFilterCondition(FilterCondition filter)
+        {
+            switch (filter)
+            {
+                case FilterCondition.Contains:
+                    return TriggerFilterCondition.Contains;
+                case FilterCondition.DoNotContain:
+                    return TriggerFilterCondition.DoNotContain;
+                case FilterCondition.EndsWith:
+                    return TriggerFilterCondition.EndsWith;
+                case FilterCondition.Equals:
+                    return TriggerFilterCondition.Equals;
+                case FilterCondition.StartsWith:
+                    return TriggerFilterCondition.StartsWith;
+                default:
+                    throw new FormatException(
+                        $"Value '{filter}' do not exist in the {typeof(TriggerFilterCondition).Name} enum.");
+            }
+        }
+
+
+        /// 
+        ///     Convert filter
+        /// 
+        /// entity
+        /// DTO
+        /// Entity enum contains a value that is currently not handled.
+        [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterResult")]
+        public static TriggerRuleAction ConvertFilterResult(FilterResult ruleAction)
+        {
+            switch (ruleAction)
+            {
+                case FilterResult.Revoke:
+                    return TriggerRuleAction.AbortTrigger;
+                case FilterResult.Continue:
+                    return TriggerRuleAction.ContinueWithNextRule;
+                case FilterResult.Grant:
+                    return TriggerRuleAction.ExecuteActions;
+                default:
+                    throw new FormatException(
+                        $"Value '{ruleAction}' do not exist in the {typeof(TriggerRuleAction).Name} enum.");
+            }
+        }
+
+        /// 
+        ///     Convert last action
+        /// 
+        /// entity
+        /// DTO
+        /// Entity enum value is not recognized.
+        public static LastTriggerActionDTO ConvertLastAction(LastTriggerAction lastTriggerAction)
+        {
+            switch (lastTriggerAction)
+            {
+                case LastTriggerAction.Revoke:
+                    return LastTriggerActionDTO.AbortTrigger;
+                case LastTriggerAction.Grant:
+                    return LastTriggerActionDTO.ExecuteActions;
+                default:
+                    throw new FormatException(
+                        $"Value '{lastTriggerAction}' do not exist in the {typeof(LastTriggerAction).Name} enum.");
+            }
+        }
+
+        /// 
+        ///     Convert all different types of rules to DTOs
+        /// 
+        /// entity
+        /// DTO
+        /// Subclass is not recognized.
+        public static TriggerRuleBase ConvertRule(RuleBase rule)
+        {
+            switch (rule)
+            {
+                case ContextCollectionRule dto:
+                    return new TriggerContextRule
+                    {
+                        ContextName = dto.ContextName,
+                        Filter = ConvertFilterCondition(dto.Condition),
+                        PropertyName = dto.PropertyName,
+                        PropertyValue = dto.PropertyValue,
+                        ResultToUse = ConvertFilterResult(dto.ResultToUse)
+                    };
+                case ExceptionRule dto1:
+                    return new TriggerExceptionRule
+                    {
+                        FieldName = dto1.FieldName,
+                        Filter = ConvertFilterCondition(dto1.Condition),
+                        ResultToUse = ConvertFilterResult(dto1.ResultToUse),
+                        Value = dto1.Value
+                    };
+            }
+
+            throw new FormatException("Failed to convert " + rule);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Queries/GetTriggerHandler.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Queries/GetTriggerHandler.cs
similarity index 85%
rename from src/Server/OneTrueError.App/Modules/Triggers/Queries/GetTriggerHandler.cs
rename to src/Server/Coderr.Server.App/Modules/Triggers/Queries/GetTriggerHandler.cs
index 948999fb..04e6230e 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Queries/GetTriggerHandler.cs
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Queries/GetTriggerHandler.cs
@@ -1,55 +1,53 @@
-using System;
-using System.Linq;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using OneTrueError.Api.Modules.Triggers.Queries;
-using OneTrueError.App.Modules.Triggers.Domain;
-
-namespace OneTrueError.App.Modules.Triggers.Queries
-{
-    /// 
-    ///     Handler for .
-    /// 
-    [Component]
-    public class GetTriggerHandler : IQueryHandler
-    {
-        private readonly ITriggerRepository _repository;
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// repos
-        /// repository
-        public GetTriggerHandler(ITriggerRepository repository)
-        {
-            if (repository == null) throw new ArgumentNullException("repository");
-            _repository = repository;
-        }
-
-        /// 
-        ///     Method used to execute the query
-        /// 
-        /// Query to execute.
-        /// 
-        ///     Task which will contain the result once completed.
-        /// 
-        public async Task ExecuteAsync(GetTrigger query)
-        {
-            var trigger = await _repository.GetAsync(query.Id);
-            return new GetTriggerDTO
-            {
-                ApplicationId = trigger.ApplicationId,
-                Actions = trigger.Actions.Select(DomainToDtoConverters.ConvertAction).ToArray(),
-                Description = trigger.Description,
-                LastTriggerAction = DomainToDtoConverters.ConvertLastAction(trigger.LastTriggerAction),
-                Id = trigger.Id,
-                Name = trigger.Name,
-                Rules = trigger.Rules.Select(DomainToDtoConverters.ConvertRule).ToArray(),
-                RunForExistingIncidents = trigger.RunForExistingIncidents,
-                RunForReOpenedIncidents = trigger.RunForReopenedIncidents,
-                RunForNewIncidents = trigger.RunForNewIncidents
-            };
-        }
-    }
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Triggers.Queries;
+using DotNetCqs;
+
+
+namespace Coderr.Server.App.Modules.Triggers.Queries
+{
+    /// 
+    ///     Handler for .
+    /// 
+    public class GetTriggerHandler : IQueryHandler
+    {
+        private readonly ITriggerRepository _repository;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repos
+        /// repository
+        public GetTriggerHandler(ITriggerRepository repository)
+        {
+            if (repository == null) throw new ArgumentNullException("repository");
+            _repository = repository;
+        }
+
+        /// 
+        ///     Method used to execute the query
+        /// 
+        /// Query to execute.
+        /// 
+        ///     Task which will contain the result once completed.
+        /// 
+        public async Task HandleAsync(IMessageContext context, GetTrigger query)
+        {
+            var trigger = await _repository.GetAsync(query.Id);
+            return new GetTriggerDTO
+            {
+                ApplicationId = trigger.ApplicationId,
+                Actions = trigger.Actions.Select(DomainToDtoConverters.ConvertAction).ToArray(),
+                Description = trigger.Description,
+                LastTriggerAction = DomainToDtoConverters.ConvertLastAction(trigger.LastTriggerAction),
+                Id = trigger.Id,
+                Name = trigger.Name,
+                Rules = trigger.Rules.Select(DomainToDtoConverters.ConvertRule).ToArray(),
+                RunForExistingIncidents = trigger.RunForExistingIncidents,
+                RunForReOpenedIncidents = trigger.RunForReopenedIncidents,
+                RunForNewIncidents = trigger.RunForNewIncidents
+            };
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/Rules/ContextCollectionRule.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Rules/ContextCollectionRule.cs
new file mode 100644
index 00000000..c2ffa76f
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Rules/ContextCollectionRule.cs
@@ -0,0 +1,25 @@
+namespace Coderr.Server.App.Modules.Triggers.Rules
+{
+    /// 
+    ///     Check a context collection in the trigger
+    /// 
+    public class ContextCollectionRule : RuleBase
+    {
+        /// 
+        ///     Context collection to check
+        /// 
+        public string ContextName { get; set; }
+
+
+        /// 
+        ///     Property in that collection
+        /// 
+        public string PropertyName { get; set; }
+
+
+        /// 
+        ///     Value for the property
+        /// 
+        public string PropertyValue { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/Rules/ExceptionRule.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Rules/ExceptionRule.cs
new file mode 100644
index 00000000..ef17db56
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Rules/ExceptionRule.cs
@@ -0,0 +1,18 @@
+namespace Coderr.Server.App.Modules.Triggers.Rules
+{
+    /// 
+    ///     Uses exception details (like Name, Message, StackTrace) to filter the trigger.
+    /// 
+    public class ExceptionRule : RuleBase
+    {
+        /// 
+        ///     Exception field name
+        /// 
+        public string FieldName { get; set; }
+
+        /// 
+        ///     Value to compare with
+        /// 
+        public string Value { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/Rules/RuleBase.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Rules/RuleBase.cs
new file mode 100644
index 00000000..ee2d7589
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Rules/RuleBase.cs
@@ -0,0 +1,18 @@
+namespace Coderr.Server.App.Modules.Triggers.Rules
+{
+    /// 
+    ///     Base for trigger rules
+    /// 
+    public class RuleBase
+    {
+        /// 
+        ///     How to compare the values
+        /// 
+        public FilterCondition Condition { get; set; }
+
+        /// 
+        ///     Result to use if value comparison succeeds.
+        /// 
+        public FilterResult ResultToUse { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Triggers/Trigger.cs b/src/Server/Coderr.Server.App/Modules/Triggers/Trigger.cs
new file mode 100644
index 00000000..2284bc7c
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Triggers/Trigger.cs
@@ -0,0 +1,135 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using Coderr.Server.App.Modules.Triggers.Rules;
+
+namespace Coderr.Server.App.Modules.Triggers
+{
+    /// 
+    ///     A filter which decides if a notification could be sent.
+    /// 
+    public class Trigger
+    {
+        private List _actions = new List();
+        private List _rules = new List();
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// application id
+        /// applicationId
+        public Trigger(int applicationId)
+        {
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
+            ApplicationId = applicationId;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected Trigger()
+        {
+        }
+
+        /// 
+        ///     Actions to take when the rules have been passed.
+        /// 
+        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Loaded by repos")]
+        public IEnumerable Actions
+        {
+            get { return _actions; }
+            private set { _actions = new List(value); }
+        }
+
+        /// 
+        ///     Application id
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Why the trigger was created and what it does
+        /// 
+        public string Description { get; set; }
+
+
+        /// 
+        ///     Identity
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     If no filters match, do this.
+        /// 
+        public LastTriggerAction LastTriggerAction { get; set; }
+
+        /// 
+        ///     Trigger name
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Rules to check
+        /// 
+        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Loaded by repos")]
+        public IEnumerable Rules
+        {
+            get { return _rules; }
+            private set { _rules = new List(value); }
+        }
+
+        /// 
+        ///     Run when we get a report for an existing incident.
+        /// 
+        public bool RunForExistingIncidents { get; set; }
+
+        /// 
+        ///     Should run for new incidents (receives a new unique exception)
+        /// 
+        public bool RunForNewIncidents { get; set; }
+
+        /// 
+        ///     Run for closed incidents that receive a new report.
+        /// 
+        public bool RunForReopenedIncidents { get; set; }
+
+        /// 
+        ///     Add a new action
+        /// 
+        /// what to do
+        public void AddAction(ActionConfigurationData actionData)
+        {
+            if (actionData == null) throw new ArgumentNullException("actionData");
+            _actions.Add(actionData);
+        }
+
+
+        /// 
+        ///     Add the rules in the order that they should be check in. the first rule added is the first rule that will decide
+        ///     which action to take.
+        /// 
+        /// Rule to add
+        public void AddRule(RuleBase rule)
+        {
+            if (rule == null) throw new ArgumentNullException("rule");
+
+            _rules.Add(rule);
+        }
+
+
+        /// 
+        ///     Remove all actions
+        /// 
+        public void RemoveActions()
+        {
+            _actions.Clear();
+        }
+
+        /// 
+        ///     Remove all rules.
+        /// 
+        public void RemoveRules()
+        {
+            _rules.Clear();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.App/Modules/Versions/NewVersionReported.cs b/src/Server/Coderr.Server.App/Modules/Versions/NewVersionReported.cs
new file mode 100644
index 00000000..bed70d60
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Versions/NewVersionReported.cs
@@ -0,0 +1,26 @@
+namespace Coderr.Server.App.Modules.Versions
+{
+    /// 
+    ///     We received a report for a new version
+    /// 
+    public class NewVersionReported
+    {
+        /// 
+        ///     Which application the version was reported for
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Name of the application
+        /// 
+        public string ApplicationName { get; set; }
+
+        /// 
+        ///     Version that we got a report for
+        /// 
+        /// 
+        ///     Formatted as a typical assembly version
+        /// 
+        public string Version { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Versions/VersionQuickFactProvider.cs b/src/Server/Coderr.Server.App/Modules/Versions/VersionQuickFactProvider.cs
new file mode 100644
index 00000000..5262a7b7
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Versions/VersionQuickFactProvider.cs
@@ -0,0 +1,36 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Abstractions.Incidents;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using Coderr.Server.Domain.Modules.ApplicationVersions;
+
+namespace Coderr.Server.App.Modules.Versions
+{
+    [ContainerService]
+    class VersionQuickFactProvider : IQuickfactProvider
+    {
+        private IApplicationVersionRepository _repository;
+
+        public VersionQuickFactProvider(IApplicationVersionRepository repository)
+        {
+            _repository = repository;
+        }
+
+        public async Task CollectAsync(QuickFactContext context)
+        {
+            var versions = await _repository.FindForIncidentAsync(context.IncidentId);
+            if (!versions.Any())
+            {
+                return;
+            }
+
+            context.CollectedFacts.Add(new QuickFact
+            {
+                Title = "Versions",
+                Description = "Application versions that this incident have been detected in.",
+                Value = string.Join(", ", versions.Select(x => "v" + x.Version))
+            });
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.App/Modules/Whitelists/IWhitelistRepository.cs b/src/Server/Coderr.Server.App/Modules/Whitelists/IWhitelistRepository.cs
new file mode 100644
index 00000000..0031c6af
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Whitelists/IWhitelistRepository.cs
@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.App.Modules.Whitelists
+{
+    /// 
+    ///     Whitelists is used for reports that don't use a shared secret
+    /// 
+    public interface IWhitelistRepository
+    {
+        Task FindIp(int applicationId, IPAddress address);
+        Task> FindWhitelists(int applicationId);
+
+        Task SaveIp(WhitelistedDomainIp entry);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Whitelists/IWhitelistService.cs b/src/Server/Coderr.Server.App/Modules/Whitelists/IWhitelistService.cs
new file mode 100644
index 00000000..eafd580a
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Whitelists/IWhitelistService.cs
@@ -0,0 +1,27 @@
+using System.Net;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.App.Modules.Whitelists
+{
+    /// 
+    ///     Used to validate origin of inbound requests when a shared secret is not used.
+    /// 
+    public interface IWhitelistService
+    {
+        /// 
+        ///     Is domain white listed?
+        /// 
+        /// AppKey used when receiving error reports.
+        /// IP address of the client reporting the error.
+        /// 
+        Task Validate(string appKey, IPAddress remoteAddress);
+
+        /// 
+        ///     Is domain white listed?
+        /// 
+        /// Application that the error is reported for.
+        /// IP address of the client reporting the error.
+        /// 
+        Task Validate(int applicationId, IPAddress remoteAddress);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Whitelists/IpType.cs b/src/Server/Coderr.Server.App/Modules/Whitelists/IpType.cs
new file mode 100644
index 00000000..fd1f9528
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Whitelists/IpType.cs
@@ -0,0 +1,23 @@
+namespace Coderr.Server.App.Modules.Whitelists
+{
+    /// 
+    ///     Typ of stored IP record.
+    /// 
+    public enum IpType
+    {
+        /// 
+        ///     Added when doing a lookup for the domain
+        /// 
+        Lookup = 0,
+
+        /// 
+        ///     Manually specified by the user
+        /// 
+        Manual = 1,
+
+        /// 
+        ///     We got a request from this IP and a lookup didn't match it.
+        /// 
+        Denied = 2
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Whitelists/Whitelist.cs b/src/Server/Coderr.Server.App/Modules/Whitelists/Whitelist.cs
new file mode 100644
index 00000000..1dcd0922
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Whitelists/Whitelist.cs
@@ -0,0 +1,28 @@
+namespace Coderr.Server.App.Modules.Whitelists
+{
+    /// 
+    ///     Domain that is allowed to report errors without
+    /// 
+    public class Whitelist
+    {
+        /// 
+        ///     Domain name, must be an exact match. Can also be an IP address
+        /// 
+        public string DomainName { get; set; }
+
+        /// 
+        ///     PK
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     Addresses that have been stored for this domain
+        /// 
+        public WhitelistedDomainIp[] IpAddresses { get; set; }
+
+        /// 
+        /// Applications that this whitelist is allowed for
+        /// 
+        public int[] ApplicationIds { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Whitelists/WhitelistService.cs b/src/Server/Coderr.Server.App/Modules/Whitelists/WhitelistService.cs
new file mode 100644
index 00000000..33190368
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Whitelists/WhitelistService.cs
@@ -0,0 +1,132 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.Applications;
+using DnsClient;
+using DnsClient.Protocol;
+
+namespace Coderr.Server.App.Modules.Whitelists
+{
+    /// 
+    ///     Used to validate origin of inbound requests when a shared secret is not used.
+    /// 
+    [ContainerService]
+    public class WhitelistService : IWhitelistService
+    {
+        private readonly IWhitelistRepository _repository;
+        private readonly IApplicationRepository _applicationRepository;
+
+        public WhitelistService(IWhitelistRepository repository, IApplicationRepository applicationRepository)
+        {
+            _repository = repository ?? throw new ArgumentNullException(nameof(repository));
+            _applicationRepository = applicationRepository;
+        }
+
+        public async Task Validate(string appKey, IPAddress remoteAddress)
+        {
+            var app = await _applicationRepository.GetByKeyAsync(appKey);
+            return await Validate(app.Id, remoteAddress);
+        }
+
+        /// 
+        ///     Is domain white listed?
+        /// 
+        /// Application that the error is reported for.
+        /// IP address of the client reporting the error.
+        /// h
+        public async Task Validate(int applicationId, IPAddress remoteAddress)
+        {
+            if (remoteAddress.IsIPv6LinkLocal || remoteAddress.IsIPv6SiteLocal || Equals(remoteAddress, IPAddress.Loopback) || Equals(remoteAddress, IPAddress.IPv6Loopback))
+            {
+                return true;
+            }
+
+            var ipEntry = await _repository.FindIp(applicationId, remoteAddress);
+            if (ipEntry != null)
+            {
+                return ipEntry.IpType != IpType.Denied;
+            }
+
+            var domains = await _repository.FindWhitelists(applicationId);
+
+            // Allow nothing if the whitelist is empty
+            if (!domains.Any())
+                return false;
+
+            foreach (var domain in domains)
+            {
+                var found = await Lookup(domain, remoteAddress);
+                if (found)
+                    return true;
+            }
+
+            foreach (var domain in domains)
+            {
+                await _repository.SaveIp(new WhitelistedDomainIp
+                {
+                    IpAddress = remoteAddress,
+                    DomainId = domain.Id,
+                    IpType = IpType.Denied,
+                    StoredAtUtc = DateTime.UtcNow
+                });
+            }
+
+            return false;
+        }
+
+        private async Task Lookup(Whitelist domain, IPAddress remoteAddress)
+        {
+            if (remoteAddress.IsInternal())
+            {
+                return true;
+            }
+
+            var found = false;
+            var lookup = new LookupClient();
+            var result = await lookup.QueryAsync(domain.DomainName, QueryType.ANY);
+            foreach (var record in result.AllRecords)
+            {
+                switch (record)
+                {
+                    case ARecord ipRecord:
+                        if (domain.IpAddresses.Any(x => Equals(x.IpAddress, ipRecord.Address)))
+                            continue;
+
+
+                        if (remoteAddress.Equals(ipRecord.Address))
+                            found = true;
+
+                        await _repository.SaveIp(new WhitelistedDomainIp
+                        {
+                            DomainId = domain.Id,
+                            IpAddress = ipRecord.Address,
+                            IpType = IpType.Lookup,
+                            StoredAtUtc = DateTime.UtcNow
+                        });
+                        break;
+
+                    case AaaaRecord ip6Record:
+                        if (domain.IpAddresses.Any(x => Equals(x.IpAddress, ip6Record.Address)))
+                            continue;
+
+                        if (remoteAddress.Equals(ip6Record.Address))
+                            found = true;
+
+                        await _repository.SaveIp(new WhitelistedDomainIp
+                        {
+                            DomainId = domain.Id,
+                            IpAddress = ip6Record.Address,
+                            IpType = IpType.Lookup,
+                            StoredAtUtc = DateTime.UtcNow
+                        });
+                        break;
+                }
+            }
+
+            return found;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/Modules/Whitelists/WhitelistedDomainIp.cs b/src/Server/Coderr.Server.App/Modules/Whitelists/WhitelistedDomainIp.cs
new file mode 100644
index 00000000..36ed90fc
--- /dev/null
+++ b/src/Server/Coderr.Server.App/Modules/Whitelists/WhitelistedDomainIp.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Net;
+
+namespace Coderr.Server.App.Modules.Whitelists
+{
+    /// 
+    ///     IP address that we have looked up (DNS lookup) for the specified domain entry.
+    /// 
+    public class WhitelistedDomainIp
+    {
+        /// 
+        /// 
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     Domain that this entry is for.
+        /// 
+        public int DomainId { get; set; }
+
+        /// 
+        ///     Address that we found
+        /// 
+        public IPAddress IpAddress { get; set; }
+
+        /// 
+        ///     How this IP should be treated
+        /// 
+        public IpType IpType { get; set; }
+
+        /// 
+        ///     When this entry was stored.
+        /// 
+        public DateTime StoredAtUtc { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.App/ReadMe.md b/src/Server/Coderr.Server.App/ReadMe.md
new file mode 100644
index 00000000..cd40f960
--- /dev/null
+++ b/src/Server/Coderr.Server.App/ReadMe.md
@@ -0,0 +1,4 @@
+App
+======
+
+Takes care of everything tha tthe user want to do either through the UI or the web API.
diff --git a/src/Server/Coderr.Server.App/codeRR.Server.App.csproj.DotSettings b/src/Server/Coderr.Server.App/codeRR.Server.App.csproj.DotSettings
new file mode 100644
index 00000000..c54c126d
--- /dev/null
+++ b/src/Server/Coderr.Server.App/codeRR.Server.App.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+	CSharp70
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Coderr.Server.Domain.csproj b/src/Server/Coderr.Server.Domain/Coderr.Server.Domain.csproj
new file mode 100644
index 00000000..291a135f
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Coderr.Server.Domain.csproj
@@ -0,0 +1,11 @@
+
+
+  
+    netstandard2.0
+    Debug;Release;Premise
+  
+
+  
+    
+  
+
diff --git a/src/Server/OneTrueError.App/Core/Accounts/Account.cs b/src/Server/Coderr.Server.Domain/Core/Account/Account.cs
similarity index 82%
rename from src/Server/OneTrueError.App/Core/Accounts/Account.cs
rename to src/Server/Coderr.Server.Domain/Core/Account/Account.cs
index d562d406..68fd62d3 100644
--- a/src/Server/OneTrueError.App/Core/Accounts/Account.cs
+++ b/src/Server/Coderr.Server.Domain/Core/Account/Account.cs
@@ -1,228 +1,257 @@
-using System;
-using System.Security.Authentication;
-using System.Security.Cryptography;
-
-namespace OneTrueError.App.Core.Accounts
-{
-    /// 
-    ///     An account (i.e. just allows a user to login, but do not give access to teams etc).
-    /// 
-    public class Account
-    {
-        /// 
-        ///     Maximum number of password attempts before account becomes locked.
-        /// 
-        public const int MaxPasswordAttempts = 3;
-
-        /// 
-        ///     Create a new instance of -
-        /// 
-        /// User name
-        /// password
-        public Account(string userName, string password)
-        {
-            if (userName == null) throw new ArgumentNullException("userName");
-            if (password == null) throw new ArgumentNullException("password");
-
-            UserName = userName;
-            CreatedAtUtc = DateTime.UtcNow;
-            ActivationKey = Guid.NewGuid().ToString("N");
-            AccountState = AccountState.VerificationRequired;
-            HashedPassword = HashNewPassword(password);
-        }
-
-        /// 
-        ///     Serialization constructor
-        /// 
-        protected Account()
-        {
-        }
-
-        /// 
-        ///     Current state
-        /// 
-        public AccountState AccountState { get; private set; }
-
-        /// 
-        ///     Used to verify the mail address (if verifiaction is activated)
-        /// 
-        public string ActivationKey { get; private set; }
-
-        /// 
-        ///     When this account was created.
-        /// 
-        public DateTime CreatedAtUtc { get; private set; }
-
-        /// 
-        ///     Private setter since new emails needs to be verifier (verification email with a link)
-        /// 
-        public string Email { get; set; }
-
-        /// 
-        ///     Password salted and hashed.
-        /// 
-        public string HashedPassword { get; private set; }
-
-
-        /// 
-        ///     Primary key
-        /// 
-        // ReSharper disable once UnusedAutoPropertyAccessor.Local
-        public int Id { get; private set; }
-
-        /// 
-        ///     IS system administrator
-        /// 
-        public bool IsSysAdmin { get; set; }
-
-        /// 
-        ///     When last successful login attempt was made.
-        /// 
-        public DateTime LastLoginAtUtc { get; private set; }
-
-        /// 
-        ///     Number of failed login attempts (reseted on each successfull login attempt).
-        /// 
-        public int LoginAttempts { get; private set; }
-
-        /// 
-        ///     Password salt.
-        /// 
-        public string Salt { get; private set; }
-
-
-        /// 
-        ///     Last time a property was updated.
-        /// 
-        public DateTime UpdatedAtUtc { get; private set; }
-
-        /// 
-        ///     Username
-        /// 
-        public string UserName { get; private set; }
-
-        /// 
-        ///     Activate account (i.e. allow logins).
-        /// 
-        public void Activate()
-        {
-            AccountState = AccountState.Active;
-            ActivationKey = null;
-            UpdatedAtUtc = DateTime.UtcNow;
-            LoginAttempts = 0;
-            LastLoginAtUtc = DateTime.UtcNow;
-        }
-
-        /// 
-        ///     Change password
-        /// 
-        /// New password as entered by the user.
-        public void ChangePassword(string newPassword)
-        {
-            if (newPassword == null) throw new ArgumentNullException("newPassword");
-            HashedPassword = HashNewPassword(newPassword);
-            ActivationKey = null;
-            UpdatedAtUtc = DateTime.UtcNow;
-            AccountState = AccountState.Active;
-            LoginAttempts = 0;
-        }
-
-        /// 
-        ///     Login
-        /// 
-        /// Password as specified by the user
-        /// true if password was the correct one; otherwise false.
-        /// Account is not active, or too many failed login attempts.
-        public bool Login(string password)
-        {
-            if (AccountState == AccountState.VerificationRequired)
-                throw new AuthenticationException("You have to activate your account first. Check your email.");
-
-            if (AccountState == AccountState.Locked)
-                throw new AuthenticationException("Your account has been locked. Contact support.");
-
-            // null for cookie logins.
-            if (password == null)
-            {
-                LastLoginAtUtc = DateTime.UtcNow;
-                LoginAttempts = 0;
-                return true;
-            }
-
-            var validPw = ValidatePassword(password);
-            if (validPw)
-            {
-                LastLoginAtUtc = DateTime.UtcNow;
-                LoginAttempts = 0;
-                return true;
-            }
-            LoginAttempts++;
-
-            //need to have it at the bottom too so that we can throw on the failed max attempt.
-            if (LoginAttempts >= MaxPasswordAttempts)
-            {
-                AccountState = AccountState.Locked;
-                throw new AuthenticationException("Too many login attempts.");
-            }
-            return false;
-        }
-
-        /// 
-        ///     Want to reset password.
-        /// 
-        /// 
-        ///     
-        ///         Changes user state to  and generates a new
-        ///         .
-        ///     
-        /// 
-        public void RequestPasswordReset()
-        {
-            AccountState = AccountState.ResetPassword;
-            ActivationKey = Guid.NewGuid().ToString("N");
-        }
-
-
-        /// 
-        ///     Email has been verified.
-        /// 
-        /// Email address
-        public void SetVerifiedEmail(string email)
-        {
-            if (email == null) throw new ArgumentNullException("email");
-            Email = email;
-        }
-
-        /// 
-        ///     Check if the given password is the current one.
-        /// 
-        /// Password as entered by the user.
-        /// true if the password is the same as the current one; otherwise false.
-        public bool ValidatePassword(string enteredPassword)
-        {
-            if (enteredPassword == null) throw new ArgumentNullException("enteredPassword");
-            var salt = Convert.FromBase64String(Salt);
-            var algorithm2 = new Rfc2898DeriveBytes(enteredPassword, salt);
-            var pw = algorithm2.GetBytes(128);
-
-            var hashedPw = Convert.ToBase64String(pw);
-            return hashedPw == HashedPassword;
-        }
-
-
-        /// 
-        ///     Hash password and generate a new salt.
-        /// 
-        /// Password as entered by the user
-        /// Salted and hashed password
-        private string HashNewPassword(string password)
-        {
-            if (password == null) throw new ArgumentNullException("password");
-            var algorithm2 = new Rfc2898DeriveBytes(password, 64);
-            var salt = algorithm2.Salt;
-            Salt = Convert.ToBase64String(salt);
-            var pw = algorithm2.GetBytes(128);
-            return Convert.ToBase64String(pw);
-        }
-    }
+using System;
+using System.Security.Authentication;
+using System.Security.Cryptography;
+
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
+
+namespace Coderr.Server.Domain.Core.Account
+{
+    /// 
+    ///     An account (i.e. just allows a user to login, but do not give access to teams etc).
+    /// 
+    public class Account
+    {
+        /// 
+        ///     Maximum number of password attempts before account becomes locked.
+        /// 
+        public const int MaxPasswordAttempts = 3;
+
+        /// 
+        ///     Create a new instance of -
+        /// 
+        /// Predefined account id
+        /// User name
+        /// password
+        public Account(int accountId, string userName, string password)
+            : this(userName, password)
+        {
+            if (accountId <= 0) throw new ArgumentOutOfRangeException(nameof(accountId));
+            Id = accountId;
+        }
+
+        /// 
+        ///     Create a new instance of -
+        /// 
+        /// User name
+        /// password
+        public Account(string userName, string password)
+        {
+            if (password == null) throw new ArgumentNullException(nameof(password));
+
+            UserName = userName ?? throw new ArgumentNullException(nameof(userName));
+            CreatedAtUtc = DateTime.UtcNow;
+            ActivationKey = Guid.NewGuid().ToString("N");
+            AccountState = AccountState.VerificationRequired;
+            HashedPassword = HashNewPassword(password);
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected Account()
+        {
+        }
+
+        /// 
+        ///     Current state
+        /// 
+        public AccountState AccountState { get; private set; }
+
+        /// 
+        ///     Used to verify the mail address (if verification is activated)
+        /// 
+        public string ActivationKey { get; private set; }
+
+        /// 
+        ///     When this account was created.
+        /// 
+        public DateTime CreatedAtUtc { get; private set; }
+
+        /// 
+        ///     Private setter since new emails needs to be verifier (verification email with a link)
+        /// 
+        public string Email { get; set; }
+
+        /// 
+        ///     Password salted and hashed.
+        /// 
+        public string HashedPassword { get; private set; }
+
+
+        /// 
+        ///     Primary key
+        /// 
+        // ReSharper disable once UnusedAutoPropertyAccessor.Local
+        public int Id { get; private set; }
+
+        /// 
+        ///     IS system administrator
+        /// 
+        public bool IsSysAdmin { get; set; }
+
+        /// 
+        ///     When last successful login attempt was made.
+        /// 
+        public DateTime LastLoginAtUtc { get; private set; }
+
+        /// 
+        ///     Number of failed login attempts (reseted on each successful login attempt).
+        /// 
+        public int LoginAttempts { get; private set; }
+
+        /// 
+        ///     Password salt.
+        /// 
+        public string Salt { get; private set; }
+
+
+        /// 
+        ///     Last time a property was updated.
+        /// 
+        public DateTime UpdatedAtUtc { get; private set; }
+
+        /// 
+        ///     User name / login name
+        /// 
+        public string UserName { get; private set; }
+
+        /// 
+        ///     Activate account (i.e. allow logins).
+        /// 
+        public void Activate()
+        {
+            AccountState = AccountState.Active;
+            ActivationKey = null;
+            UpdatedAtUtc = DateTime.UtcNow;
+            LoginAttempts = 0;
+            LastLoginAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        ///     Change password
+        /// 
+        /// New password as entered by the user.
+        public void ChangePassword(string newPassword)
+        {
+            if (newPassword == null) throw new ArgumentNullException(nameof(newPassword));
+            HashedPassword = HashNewPassword(newPassword);
+            ActivationKey = null;
+            UpdatedAtUtc = DateTime.UtcNow;
+            AccountState = AccountState.Active;
+            LoginAttempts = 0;
+        }
+
+        /// 
+        ///     Login
+        /// 
+        /// Password as specified by the user
+        /// true if password was the correct one; otherwise false.
+        /// Account is not active, or too many failed login attempts.
+        public bool Login(string password)
+        {
+            if (AccountState == AccountState.VerificationRequired)
+                throw new AuthenticationException("You have to activate your account first. Check your email.");
+
+            if (AccountState == AccountState.Locked)
+                throw new AuthenticationException("Your account has been locked. Contact support.");
+
+            // null for cookie logins.
+            if (password == null)
+            {
+                LastLoginAtUtc = DateTime.UtcNow;
+                LoginAttempts = 0;
+                return true;
+            }
+
+            var validPw = ValidatePassword(password);
+            if (validPw)
+            {
+                LastLoginAtUtc = DateTime.UtcNow;
+                LoginAttempts = 0;
+                return true;
+            }
+            LoginAttempts++;
+
+            //need to have it at the bottom too so that we can throw on the failed max attempt.
+            if (LoginAttempts >= MaxPasswordAttempts)
+            {
+                AccountState = AccountState.Locked;
+                throw new AuthenticationException("Too many login attempts.");
+            }
+            return false;
+        }
+        
+        /// 
+        /// Mark user as logged in.
+        /// 
+        public void SingleSignOn()
+        {
+            LastLoginAtUtc = DateTime.UtcNow;
+            LoginAttempts = 0;
+            AccountState = AccountState.Active;
+        }
+
+        /// 
+        ///     Want to reset password.
+        /// 
+        /// 
+        ///     
+        ///         Changes user state to  and generates a new
+        ///         .
+        ///     
+        /// 
+        public void RequestPasswordReset()
+        {
+            AccountState = AccountState.ResetPassword;
+            ActivationKey = Guid.NewGuid().ToString("N");
+        }
+
+
+        /// 
+        ///     Email has been verified.
+        /// 
+        /// Email address
+        public void SetVerifiedEmail(string email)
+        {
+            Email = email ?? throw new ArgumentNullException(nameof(email));
+
+            // To enable UI testing without email integration
+            if (email.EndsWith("test@localhost.com"))
+            {
+                ActivationKey = "abc123";
+            }
+        }
+
+        /// 
+        ///     Check if the given password is the current one.
+        /// 
+        /// Password as entered by the user.
+        /// true if the password is the same as the current one; otherwise false.
+        public bool ValidatePassword(string enteredPassword)
+        {
+            if (enteredPassword == null) throw new ArgumentNullException(nameof(enteredPassword));
+            var salt = Convert.FromBase64String(Salt);
+            var algorithm2 = new Rfc2898DeriveBytes(enteredPassword, salt);
+            var pw = algorithm2.GetBytes(128);
+
+            var hashedPw = Convert.ToBase64String(pw);
+            return hashedPw == HashedPassword;
+        }
+
+
+        /// 
+        ///     Hash password and generate a new salt.
+        /// 
+        /// Password as entered by the user
+        /// Salted and hashed password
+        private string HashNewPassword(string password)
+        {
+            if (password == null) throw new ArgumentNullException(nameof(password));
+            var algorithm2 = new Rfc2898DeriveBytes(password, 64);
+            var salt = algorithm2.Salt;
+            Salt = Convert.ToBase64String(salt);
+            var pw = algorithm2.GetBytes(128);
+            return Convert.ToBase64String(pw);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Core/Accounts/AccountState.cs b/src/Server/Coderr.Server.Domain/Core/Account/AccountState.cs
similarity index 90%
rename from src/Server/OneTrueError.App/Core/Accounts/AccountState.cs
rename to src/Server/Coderr.Server.Domain/Core/Account/AccountState.cs
index 79cd1807..1a149d0f 100644
--- a/src/Server/OneTrueError.App/Core/Accounts/AccountState.cs
+++ b/src/Server/Coderr.Server.Domain/Core/Account/AccountState.cs
@@ -1,28 +1,28 @@
-namespace OneTrueError.App.Core.Accounts
-{
-    /// 
-    ///     Account state
-    /// 
-    public enum AccountState
-    {
-        /// 
-        ///     Account have been created but not yet verified.
-        /// 
-        VerificationRequired,
-
-        /// 
-        ///     Account is active
-        /// 
-        Active,
-
-        /// 
-        ///     Account have been locked, typically by too many login attempts.
-        /// 
-        Locked,
-
-        /// 
-        ///     Password reset have been requested (an password reset link have been sent).
-        /// 
-        ResetPassword
-    }
+namespace Coderr.Server.Domain.Core.Account
+{
+    /// 
+    ///     Account state
+    /// 
+    public enum AccountState
+    {
+        /// 
+        ///     Account have been created but not yet verified.
+        /// 
+        VerificationRequired,
+
+        /// 
+        ///     Account is active
+        /// 
+        Active,
+
+        /// 
+        ///     Account have been locked, typically by too many login attempts.
+        /// 
+        Locked,
+
+        /// 
+        ///     Password reset have been requested (an password reset link have been sent).
+        /// 
+        ResetPassword
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Core/Accounts/IAccountRepository.cs b/src/Server/Coderr.Server.Domain/Core/Account/IAccountRepository.cs
similarity index 78%
rename from src/Server/OneTrueError.App/Core/Accounts/IAccountRepository.cs
rename to src/Server/Coderr.Server.Domain/Core/Account/IAccountRepository.cs
index f6574e6b..16e42486 100644
--- a/src/Server/OneTrueError.App/Core/Accounts/IAccountRepository.cs
+++ b/src/Server/Coderr.Server.Domain/Core/Account/IAccountRepository.cs
@@ -1,82 +1,94 @@
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Griffin.Data;
-
-namespace OneTrueError.App.Core.Accounts
-{
-    /// 
-    ///     Repository for accounts
-    /// 
-    public interface IAccountRepository
-    {
-        /// 
-        ///     Create a new account.
-        /// 
-        /// account
-        /// task
-        /// 
-        ///     UserName and email address must be unique
-        /// 
-        Task CreateAsync(Account account);
-
-        /// 
-        ///     find by using the activation key
-        /// 
-        /// 
-        /// account if found; otherwise null.
-        Task FindByActivationKeyAsync(string activationKey);
-
-        /// 
-        ///     find user by using email.
-        /// 
-        /// email
-        /// account if found; otherwise null.
-        Task FindByEmailAsync(string emailAddress);
-
-        /// 
-        ///     Find user
-        /// 
-        /// username to match
-        /// account if found; otherwise null.
-        Task FindByUserNameAsync(string userName);
-
-        /// 
-        ///     Get account by id
-        /// 
-        /// account id
-        /// account
-        /// No account exists with the given id.
-        Task GetByIdAsync(int id);
-
-
-        /// 
-        ///     Get all accounts by the given ids
-        /// 
-        /// account ids
-        /// Corresponding accounts
-        /// One or more of the given ids did not have a matching account..
-        Task> GetByIdAsync(int[] ids);
-
-        /// 
-        ///     Check if email address is taken
-        /// 
-        /// email
-        /// true if it exists; otherwise false.
-        Task IsEmailAddressTakenAsync(string email);
-
-
-        /// 
-        ///     Check if username is already taken.
-        /// 
-        /// Username
-        /// true if it exists; otherwise false.
-        Task IsUserNameTakenAsync(string userName);
-
-        /// 
-        ///     Update account
-        /// 
-        /// account
-        /// task
-        Task UpdateAsync(Account account);
-    }
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Core.Account
+{
+    /// 
+    ///     Repository for accounts
+    /// 
+    public interface IAccountRepository
+    {
+        /// 
+        ///     Count the number of created accounts.
+        /// 
+        /// 
+        Task CountAsync();
+
+        /// 
+        ///     Create a new account.
+        /// 
+        /// account
+        /// task
+        /// 
+        ///     UserName and email address must be unique
+        /// 
+        Task CreateAsync(Account account);
+
+        /// 
+        ///     find by using the activation key
+        /// 
+        /// 
+        /// account if found; otherwise null.
+        Task FindByActivationKeyAsync(string activationKey);
+
+        /// 
+        ///     find user by using email.
+        /// 
+        /// email
+        /// account if found; otherwise null.
+        Task FindByEmailAsync(string emailAddress);
+
+        /// 
+        ///     Find user
+        /// 
+        /// user name to match
+        /// account if found; otherwise null.
+        Task FindByUserNameAsync(string userName);
+
+        /// 
+        ///     Get account by id
+        /// 
+        /// account id
+        /// account
+        /// No account exists with the given id.
+        Task GetByIdAsync(int id);
+
+        /// 
+        /// Get user by user name
+        /// 
+        /// user name
+        /// user
+        /// No account exists with the given userName.
+        Task GetByUserNameAsync(string userName);
+
+        /// 
+        ///     Get all accounts by the given ids
+        /// 
+        /// account ids
+        /// Corresponding accounts
+        /// One or more of the given ids did not have a matching account..
+        Task> GetByIdAsync(int[] ids);
+
+        /// 
+        ///     Check if email address is taken
+        /// 
+        /// email
+        /// true if it exists; otherwise false.
+        Task IsEmailAddressTakenAsync(string email);
+
+
+        /// 
+        ///     Check if user name is already taken.
+        /// 
+        /// User name
+        /// true if it exists; otherwise false.
+        Task IsUserNameTakenAsync(string userName);
+
+        /// 
+        ///     Update account
+        /// 
+        /// account
+        /// task
+        Task UpdateAsync(Account account);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Applications/Application.cs b/src/Server/Coderr.Server.Domain/Core/Applications/Application.cs
new file mode 100644
index 00000000..8643e2a9
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Applications/Application.cs
@@ -0,0 +1,121 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.Domain.Core.Applications
+{
+    /// 
+    ///     An application which we can receive exceptions from.
+    /// 
+    public class Application
+    {
+        private bool _muteStatisticsQuestion;
+
+        /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        /// Account id for the user that created this application.
+        /// Application name as defined by the user.
+        public Application(int createdById, string name)
+        {
+            if (createdById < 1) throw new ArgumentNullException("createdById");
+            if (name == null) throw new ArgumentNullException("name");
+
+            CreatedById = createdById;
+            AppKey = Guid.NewGuid().ToString("N");
+            Name = name;
+            CreatedAtUtc = DateTime.UtcNow;
+            SharedSecret = Guid.NewGuid().ToString("N");
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected Application()
+        {
+        }
+
+        /// 
+        ///     Gets or sets ID used to identify this application
+        /// 
+        /// 
+        ///     The application id are used in the query string when reports are sent. It's then used to find the correct
+        ///     application so that we can decrypt using the shared secret.
+        /// 
+        public string AppKey { get; set; }
+
+        /// 
+        ///     Defines the type of application
+        /// 
+        /// 
+        ///     Used to configure how the analysis should be made.
+        /// 
+        public TypeOfApplication ApplicationType { get; set; }
+
+        /// 
+        ///     When the application was created.
+        /// 
+        public DateTime CreatedAtUtc { get; private set; }
+
+        /// 
+        ///     Account id for the user that created the application.
+        /// 
+        public int CreatedById { get; set; }
+
+        /// 
+        ///     Gets db identifier used in relations.
+        /// 
+        // ReSharper disable once UnusedAutoPropertyAccessor.Local, set by reflection
+        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode",
+            Justification = "Set by reflection.")]
+        // ReSharper disable once UnusedAutoPropertyAccessor.Local
+        public int Id { get; private set; }
+
+        /// 
+        ///     Gets title
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Gets or sets the shared secret which is used to encrypt sensitive data between the reporter client and the server.
+        /// 
+        /// The user have to manually configure the secret.
+        public string SharedSecret { get; private set; }
+
+        /// 
+        /// Entered when the user created the application
+        /// 
+        public int? EstimatedNumberOfErrors { get; private set; }
+
+        /// 
+        /// Number of full time developers
+        /// 
+        public decimal? NumberOfFtes { get; private set; }
+
+        /// 
+        /// UI group that the application should be displayed under.
+        /// 
+        public int GroupId { get; set; }
+
+        /// 
+        /// Number of days to keep incidents.
+        /// 
+        public int RetentionDays { get; set; }
+
+        public void AddStatsBase(decimal? fte, int? numberOfErrors)
+        {
+            EstimatedNumberOfErrors = numberOfErrors;
+            NumberOfFtes = fte;
+        }
+
+        /// 
+        /// Don't ask for statistics information
+        /// 
+        public bool MuteStatisticsQuestion
+        {
+            get => _muteStatisticsQuestion || NumberOfFtes > 0 || EstimatedNumberOfErrors > 0;
+            set => _muteStatisticsQuestion = value;
+        }
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Core/Applications/ApplicationRole.cs b/src/Server/Coderr.Server.Domain/Core/Applications/ApplicationRole.cs
similarity index 87%
rename from src/Server/OneTrueError.App/Core/Applications/ApplicationRole.cs
rename to src/Server/Coderr.Server.Domain/Core/Applications/ApplicationRole.cs
index 2d0d51f0..674a7c98 100644
--- a/src/Server/OneTrueError.App/Core/Applications/ApplicationRole.cs
+++ b/src/Server/Coderr.Server.Domain/Core/Applications/ApplicationRole.cs
@@ -1,18 +1,18 @@
-namespace OneTrueError.App.Core.Applications
-{
-    /// 
-    ///     Roles for .
-    /// 
-    public static class ApplicationRole
-    {
-        /// 
-        ///     Can change configuration information like members and triggers.
-        /// 
-        public const string Admin = "Admin";
-
-        /// 
-        ///     Can handle incidents (ignore, close etc).
-        /// 
-        public const string Member = "Member";
-    }
+namespace Coderr.Server.Domain.Core.Applications
+{
+    /// 
+    ///     Roles for .
+    /// 
+    public static class ApplicationRole
+    {
+        /// 
+        ///     Can change configuration information like members and triggers.
+        /// 
+        public const string Admin = "Admin";
+
+        /// 
+        ///     Can handle incidents (ignore, close etc).
+        /// 
+        public const string Member = "Member";
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Core/Users/ApplicationTeamMember.cs b/src/Server/Coderr.Server.Domain/Core/Applications/ApplicationTeamMember.cs
similarity index 96%
rename from src/Server/OneTrueError.App/Core/Users/ApplicationTeamMember.cs
rename to src/Server/Coderr.Server.Domain/Core/Applications/ApplicationTeamMember.cs
index 871d0605..4dfc7e7f 100644
--- a/src/Server/OneTrueError.App/Core/Users/ApplicationTeamMember.cs
+++ b/src/Server/Coderr.Server.Domain/Core/Applications/ApplicationTeamMember.cs
@@ -1,111 +1,113 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-
-namespace OneTrueError.App.Core.Users
-{
-    /// 
-    ///     Used to control access to a specific application.
-    /// 
-    public class ApplicationTeamMember
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// Application that the user is a member of.
-        /// Email address to the member
-        /// 
-        ///     
-        ///         this constructor is used when the user have no account (invite user)
-        ///     
-        /// 
-        public ApplicationTeamMember(int applicationId, string emailAddress)
-        {
-            if (applicationId <= 0) throw new ArgumentNullException("applicationId");
-            if (emailAddress == null) throw new ArgumentNullException("emailAddress");
-            ApplicationId = applicationId;
-            EmailAddress = emailAddress;
-            AddedAtUtc = DateTime.UtcNow;
-        }
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// Application that the user is a member of.
-        /// User exist in the system
-        /// 
-        ///     
-        ///         this constructor is used when the user have an account.
-        ///     
-        /// 
-        public ApplicationTeamMember(int applicationId, int accountId)
-        {
-            if (applicationId <= 0) throw new ArgumentNullException("applicationId");
-            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
-
-            ApplicationId = applicationId;
-            AccountId = accountId;
-            AddedAtUtc = DateTime.UtcNow;
-            EmailAddress = "";
-        }
-
-        /// 
-        ///     Serialization constructor
-        /// 
-        protected ApplicationTeamMember()
-        {
-        }
-
-        /// 
-        ///     0 for invited users that do not have an existing account (and have not accepted the invitation yet).
-        /// 
-        public int AccountId { get; private set; }
-
-        /// 
-        ///     When the member was added, or when the invitation was sent.
-        /// 
-        public DateTime AddedAtUtc { get; private set; }
-
-        /// 
-        ///     User who added or invited this user.
-        /// 
-        public string AddedByName { get; set; }
-
-        /// 
-        ///     Application that the user is a member of.
-        /// 
-        public int ApplicationId { get; private set; }
-
-        /// 
-        ///     Email address (should only be used if this is an invite for a non-existing user).
-        /// 
-        public string EmailAddress { get; private set; }
-
-        /// 
-        ///     PK for the mapping
-        /// 
-        public int Id { get; set; }
-
-        /// 
-        ///     Currently assigned roles
-        /// 
-        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays",
-            Justification = "I like my arrays.")]
-        public string[] Roles { get; set; }
-
-        /// 
-        ///     Only used when fetching a member
-        /// 
-        public string UserName { get; set; }
-
-        /// 
-        ///     Invitation have been accepted.
-        /// 
-        /// 
-        public void AcceptInvitation(int accountId)
-        {
-            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
-            AccountId = accountId;
-            EmailAddress = null;
-        }
-    }
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.Domain.Core.Applications
+{
+    /// 
+    ///     Used to control access to a specific application.
+    /// 
+    public class ApplicationTeamMember
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Application that the user is a member of.
+        /// Email address to the member
+        /// 
+        ///     
+        ///         this constructor is used when the user have no account (invite user)
+        ///     
+        /// 
+        public ApplicationTeamMember(int applicationId, string emailAddress)
+        {
+            if (applicationId <= 0) throw new ArgumentNullException("applicationId");
+            if (emailAddress == null) throw new ArgumentNullException("emailAddress");
+            ApplicationId = applicationId;
+            EmailAddress = emailAddress;
+            AddedAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Application that the user is a member of.
+        /// User exist in the system
+        /// 
+        ///     
+        ///         this constructor is used when the user have an account.
+        ///     
+        /// 
+        public ApplicationTeamMember(int applicationId, int accountId, string addedByName)
+        {
+            if (applicationId <= 0) throw new ArgumentNullException("applicationId");
+            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
+
+            ApplicationId = applicationId;
+            AccountId = accountId;
+            AddedAtUtc = DateTime.UtcNow;
+            EmailAddress = "";
+            AddedByName = addedByName;
+            Roles = new[] {"Member"};
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected ApplicationTeamMember()
+        {
+        }
+
+        /// 
+        ///     0 for invited users that do not have an existing account (and have not accepted the invitation yet).
+        /// 
+        public int AccountId { get; private set; }
+
+        /// 
+        ///     When the member was added, or when the invitation was sent.
+        /// 
+        public DateTime AddedAtUtc { get; private set; }
+
+        /// 
+        ///     User who added or invited this user.
+        /// 
+        public string AddedByName { get; set; }
+
+        /// 
+        ///     Application that the user is a member of.
+        /// 
+        public int ApplicationId { get; private set; }
+
+        /// 
+        ///     Email address (should only be used if this is an invite for a non-existing user).
+        /// 
+        public string EmailAddress { get; private set; }
+
+        /// 
+        ///     PK for the mapping
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     Currently assigned roles
+        /// 
+        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays",
+            Justification = "I like my arrays.")]
+        public string[] Roles { get; set; }
+
+        /// 
+        ///     Only used when fetching a member
+        /// 
+        public string UserName { get; set; }
+
+        /// 
+        ///     Invitation have been accepted.
+        /// 
+        /// 
+        public void AcceptInvitation(int accountId)
+        {
+            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
+            AccountId = accountId;
+            EmailAddress = null;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Core/Applications/IApplicationRepository.cs b/src/Server/Coderr.Server.Domain/Core/Applications/IApplicationRepository.cs
similarity index 89%
rename from src/Server/OneTrueError.App/Core/Applications/IApplicationRepository.cs
rename to src/Server/Coderr.Server.Domain/Core/Applications/IApplicationRepository.cs
index a1a12595..e49a860f 100644
--- a/src/Server/OneTrueError.App/Core/Applications/IApplicationRepository.cs
+++ b/src/Server/Coderr.Server.Domain/Core/Applications/IApplicationRepository.cs
@@ -1,103 +1,111 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Threading.Tasks;
-using Griffin.Data;
-using OneTrueError.App.Core.Users;
-
-namespace OneTrueError.App.Core.Applications
-{
-    /// 
-    ///     Repository for application management.
-    /// 
-    public interface IApplicationRepository
-    {
-        /// 
-        ///     Create app.
-        /// 
-        /// Application to create
-        /// task
-        /// application
-        Task CreateAsync(Application application);
-
-        /// 
-        ///     Create member async
-        /// 
-        /// member
-        /// task
-        /// application
-        Task CreateAsync(ApplicationTeamMember member);
-
-        /// 
-        ///     Delete application
-        /// 
-        /// application id
-        /// task
-        /// applicationId
-        Task DeleteAsync(int applicationId);
-
-        /// 
-        ///     Get all applications
-        /// 
-        /// apps
-        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
-        Task GetAllAsync();
-
-        /// 
-        ///     Get application
-        /// 
-        /// application id
-        /// application
-        /// No application exist with the given application id
-        /// applicationId
-        Task GetByIdAsync(int applicationId);
-
-        /// 
-        ///     Get by application key
-        /// 
-        /// application key
-        /// application
-        /// appKey
-        /// No application exist with the given key.
-        Task GetByKeyAsync(string appKey);
-
-        /// 
-        ///     Get all applications that the user is a member of.
-        /// 
-        /// accountId
-        /// applications
-        /// applicationId
-        Task GetForUserAsync(int accountId);
-
-        /// 
-        ///     Get all members of an application
-        /// 
-        /// applicationID
-        /// 
-        /// applicationId
-        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
-        Task> GetTeamMembersAsync(int applicationId);
-
-        /// 
-        ///     remove a member from an application
-        /// 
-        /// app
-        /// user
-        /// task
-        Task RemoveTeamMemberAsync(int applicationId, int userId);
-
-        /// 
-        ///     Update application member
-        /// 
-        /// member
-        /// task
-        Task UpdateAsync(ApplicationTeamMember member);
-
-        /// 
-        ///     Update application.
-        /// 
-        /// app
-        /// task
-        Task UpdateAsync(Application entity);
-    }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Core.Applications
+{
+    /// 
+    ///     Repository for application management.
+    /// 
+    public interface IApplicationRepository
+    {
+        /// 
+        ///     Create app.
+        /// 
+        /// Application to create
+        /// task
+        /// application
+        Task CreateAsync(Application application);
+
+        /// 
+        ///     Create member async
+        /// 
+        /// member
+        /// task
+        /// application
+        Task CreateAsync(ApplicationTeamMember member);
+
+        /// 
+        ///     Delete application
+        /// 
+        /// application id
+        /// task
+        /// applicationId
+        Task DeleteAsync(int applicationId);
+
+        /// 
+        ///     Get all applications
+        /// 
+        /// apps
+        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
+        Task GetAllAsync();
+
+        /// 
+        ///     Get application
+        /// 
+        /// application id
+        /// application
+        /// No application exist with the given application id
+        /// applicationId
+        Task GetByIdAsync(int applicationId);
+
+        /// 
+        ///     Get by application key
+        /// 
+        /// application key
+        /// application
+        /// appKey
+        /// No application exist with the given key.
+        Task GetByKeyAsync(string appKey);
+
+        /// 
+        ///     Get all applications that the user is a member of.
+        /// 
+        /// accountId
+        /// applications
+        /// applicationId
+        Task GetForUserAsync(int accountId);
+
+        /// 
+        ///     Get all members of an application
+        /// 
+        /// applicationID
+        /// 
+        /// applicationId
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        Task> GetTeamMembersAsync(int applicationId);
+
+        /// 
+        ///     remove a member from an application
+        /// 
+        /// app
+        /// user
+        /// task
+        Task RemoveTeamMemberAsync(int applicationId, int userId);
+
+        /// 
+        ///     remove an invited member from an application
+        /// 
+        /// app
+        /// address that the invitation was sent to
+        /// task
+        Task RemoveTeamMemberAsync(int applicationId, string invitedEmailAddress);
+
+        /// 
+        ///     Update application member
+        /// 
+        /// member
+        /// task
+        Task UpdateAsync(ApplicationTeamMember member);
+
+        /// 
+        ///     Update application.
+        /// 
+        /// app
+        /// task
+        Task UpdateAsync(Application entity);
+
+        Task GetFirstGroupIdAsync();
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Applications/TypeOfApplication.cs b/src/Server/Coderr.Server.Domain/Core/Applications/TypeOfApplication.cs
new file mode 100644
index 00000000..0c275f7a
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Applications/TypeOfApplication.cs
@@ -0,0 +1,38 @@
+namespace Coderr.Server.Domain.Core.Applications
+{
+    /// 
+    ///     Kind of application that this is
+    /// 
+    /// 
+    ///     
+    ///         Used to determine how different analytics should be made, like analyzing memory usage (which has to guess the
+    ///         total amount of memory if not included as context information).
+    ///     
+    ///     
+    ///         For instance a OutOfMemoryException isn't as fatal in a mobile application, like it is in a large server
+    ///         application, as the latter is supposed to have large amount of resources.
+    ///     
+    /// 
+    public enum TypeOfApplication
+    {
+        /// 
+        ///     Cellphone application
+        /// 
+        /// 
+        ///     
+        ///         An application with limited system resources (memory and usage).
+        ///     
+        /// 
+        Mobile,
+
+        /// 
+        ///     DesktopApplication application (i.e. a windows end user computer)
+        /// 
+        DesktopApplication,
+
+        /// 
+        ///     Server, as a web server or a WCF service.
+        /// 
+        Server
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Applications/UserApplication.cs b/src/Server/Coderr.Server.Domain/Core/Applications/UserApplication.cs
new file mode 100644
index 00000000..acc3ce8d
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Applications/UserApplication.cs
@@ -0,0 +1,38 @@
+namespace Coderr.Server.Domain.Core.Applications
+{
+    /// 
+    /// Application that a specific user is a member of
+    /// 
+    public class UserApplication
+    {
+        /// 
+        /// App name
+        /// 
+        public string ApplicationName { get; set; }
+
+        /// 
+        /// ID
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        /// Logical group (used to organize applications).
+        /// 
+        public int GroupId { get; set; }
+
+        /// 
+        /// Name of the group.
+        /// 
+        public string GroupName { get; set; }
+            
+        /// 
+        /// If the user that this app is requested for is the admin
+        /// 
+        public bool IsAdmin { get; set; }
+
+        /// 
+        /// Number of full time developers.
+        /// 
+        public decimal? NumberOfDevelopers { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportContextCollection.cs b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportContextCollection.cs
new file mode 100644
index 00000000..b8defa52
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportContextCollection.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.Domain.Core.ErrorReports
+{
+    /// 
+    ///     Context used when analysing the report
+    /// 
+    public class ErrorReportContextCollection
+    {
+        private IDictionary _properties;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// context collection name
+        /// properties for the collection
+        /// name; properties
+        public ErrorReportContextCollection(string name, IDictionary properties)
+        {
+            if (name == null) throw new ArgumentNullException("name");
+            if (properties == null) throw new ArgumentNullException(nameof(properties));
+
+            Name = name;
+            Properties = properties;
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        protected ErrorReportContextCollection()
+        {
+        }
+
+        /// 
+        ///     Context collection name
+        /// 
+        public string Name { get; private set; }
+
+        /// 
+        ///     Context collection properties
+        /// 
+        public IDictionary Properties
+        {
+            get { return _properties; }
+            private set
+            {
+                if (value == null)
+                    throw new ArgumentNullException("value");
+                _properties = value;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportEntity.cs b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportEntity.cs
new file mode 100644
index 00000000..83b51dc6
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportEntity.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Text.RegularExpressions;
+
+// ReSharper disable AutoPropertyCanBeMadeGetOnly.Local
+
+namespace Coderr.Server.Domain.Core.ErrorReports
+{
+    /// 
+    ///     Represents the incoming error report, unmodified (to allow us to do further processing in the future)
+    /// 
+    /// 
+    ///     
+    ///         Important! The entity suffers from temporal coupling since we do not want to generate the hash code in the
+    ///         receiving web API but in the core windows service. As such
+    ///         the report can't be identified until the windows service has received it from the queue.
+    ///     
+    /// 
+    public class ErrorReportEntity
+    {
+        private const string RemoveLineNumbersRegEx = @"^(.*)(:[\w]+ [\d]+)";
+        List _collections = new List();
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Application that the report belongs to
+        /// error id generated by the client library
+        /// when the client library created the report
+        /// exception
+        /// context collections
+        /// 
+        /// 
+        public ErrorReportEntity(int applicationId, string clientReportId, DateTime createdAtUtc,
+            ErrorReportException exception, IEnumerable contexts)
+        {
+            if (clientReportId == null) throw new ArgumentNullException("clientReportId");
+            if (exception == null) throw new ArgumentNullException("exception");
+            if (contexts == null) throw new ArgumentNullException("contexts");
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
+
+            ClientReportId = clientReportId;
+            ApplicationId = applicationId;
+            CreatedAtUtc = createdAtUtc;
+            Exception = exception;
+            //GenerateHashCodeIdentifier();
+            _collections.AddRange(contexts);
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected ErrorReportEntity()
+        {
+        }
+
+        /// 
+        ///     PK of the application that this entity is reported for
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Used to identify this incident when the hash code is the same as for other incidents.
+        /// 
+        /// 
+        ///     Gets or sets id from the client library
+        /// 
+        public string ClientReportId { get; private set; }
+
+        /// 
+        ///     Context collection
+        /// 
+        public IReadOnlyList ContextCollections
+        {
+            get { return _collections; }
+        }
+
+        /// 
+        ///     When this entity was created (in the server)
+        /// 
+        public DateTime CreatedAtUtc { get; private set; }
+
+        /// 
+        ///     Thrown exception
+        /// 
+        public ErrorReportException Exception { get; set; }
+
+        /// 
+        ///     PK
+        /// 
+        public int Id { get; private set; }
+
+        /// 
+        ///     Gets incident that this report belongs to.
+        /// 
+        public int IncidentId { get; set; }
+
+        /// 
+        ///     Remote address from where the report was received.
+        /// 
+        public string RemoteAddress { get; set; }
+
+
+        /// 
+        ///     Hash code generated for the exception.
+        /// 
+        /// 
+        ///     Be aware that multiple different incidents (yes,  may have the same hash code).
+        /// 
+        public string ReportHashCode { get; private set; }
+
+        /// 
+        ///     Denormalization to be able to generate lists quicker (this is really Exception.Message)
+        /// 
+        public string Title { get; set; }
+
+
+        /// 
+        ///     User/Site/application that the report is for.
+        /// 
+        public ClaimsPrincipal User { get; set; }
+
+        /// 
+        /// System environment ("Production", "Test" etc") that the error was reported in.
+        /// 
+        public string EnvironmentName { get; set; }
+
+        /// 
+        ///     Used when we get hash code collisions to identify the correct incident.
+        /// 
+        public string GenerateHashCodeIdentifier()
+        {
+            var identifier = Exception.FullName + "\r\n";
+            if (string.IsNullOrEmpty(Exception.StackTrace))
+                return identifier;
+
+            var trace = StripLineNumbers(Exception.StackTrace);
+            var pos = trace.IndexOf("\r\n", StringComparison.Ordinal);
+            if (pos != -1)
+                identifier += trace.Substring(0, pos);
+
+            return identifier;
+        }
+
+        public string GenerateHashCodeIdentifier2()
+        {
+
+            var hashSource = $"{Exception.FullName ?? Exception.Name}\r\n";
+            var foundHashSource = false;
+
+            // the client libraries can by themselves specify how we should identify
+            // unique incidents. We then use that identifier in combination with the exception name.
+            var collection = ContextCollections.FirstOrDefault(x => x.Name == "CoderrData");
+            if (collection != null)
+            {
+                if (collection.Properties.TryGetValue("HashSource", out var reportHashSource))
+                {
+                    foundHashSource = true;
+                    hashSource += reportHashSource;
+                }
+                else
+                {
+                    var trace = StripLineNumbers(Exception.StackTrace);
+                    var pos = trace.IndexOf("\r\n", StringComparison.Ordinal);
+                    if (pos != -1)
+                        hashSource += trace.Substring(0, pos);
+                }
+            }
+            if (!foundHashSource)
+            {
+                // This identifier is determined by the developer when  the error is generated.
+                foreach (var contextCollection in ContextCollections)
+                {
+                    if (!contextCollection.Properties.TryGetValue("ErrorHashSource", out var ourHashSource))
+                        continue;
+
+                    hashSource = ourHashSource;
+                    break;
+                }
+            }
+
+            var hash = 23;
+            foreach (var c in hashSource)
+            {
+                hash = hash * 31 + c;
+            }
+            return hash.ToString("X");
+        }
+        private static string StripLineNumbers(string stacktrace)
+        {
+            var re = new Regex(RemoveLineNumbersRegEx, RegexOptions.Multiline);
+            return re.Replace(stacktrace, "$1", 1000);
+        }
+
+        /// 
+        ///     Temporal coupling, but the only way I could figure out.
+        /// 
+        /// hashcode used to see if this is an unique exception
+        /// hashCode
+        public void Init(string hashCode)
+        {
+            if (hashCode == null) throw new ArgumentNullException("hashCode");
+            ReportHashCode = hashCode;
+        }
+
+        /// Returns a string that represents the current object.
+        /// A string that represents the current object.
+        /// 2
+        public override string ToString()
+        {
+            return Exception != null ? Exception.Message : "Exception was not included";
+        }
+
+        public void Add(ErrorReportContextCollection collection)
+        {
+            if (collection == null) throw new ArgumentNullException(nameof(collection));
+            _collections.Add(collection);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportException.cs b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportException.cs
similarity index 94%
rename from src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportException.cs
rename to src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportException.cs
index 1f95d0a5..903fff71 100644
--- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportException.cs
+++ b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ErrorReportException.cs
@@ -1,81 +1,81 @@
-using System;
-
-namespace OneTrueError.ReportAnalyzer.Domain.Reports
-{
-    /// 
-    ///     Exception for .
-    /// 
-    public class ErrorReportException
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        public ErrorReportException()
-        {
-        }
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// exception that this entity represents
-        /// exception
-        public ErrorReportException(Exception exception)
-        {
-            if (exception == null) throw new ArgumentNullException("exception");
-            FullName = exception.GetType().FullName;
-            Name = exception.GetType().Name;
-            Name = exception.GetType().Namespace;
-            AssemblyName = exception.GetType().Assembly.GetName().Name;
-            StackTrace = exception.StackTrace;
-            BaseClasses = exception.GetType().BaseType != null
-                ? new[] {exception.GetType().BaseType.FullName}
-                : new string[0];
-            Everything = exception.ToString();
-        }
-
-        /// 
-        ///     Assembly containing the exception
-        /// 
-        public string AssemblyName { get; set; }
-
-        /// 
-        ///     Class names of the base classes (minimum 'Exception').
-        /// 
-        public string[] BaseClasses { get; set; }
-
-        /// 
-        ///     Exception.ToString()
-        /// 
-        public string Everything { get; set; }
-
-        /// 
-        ///     Full type name
-        /// 
-        public string FullName { get; set; }
-
-        /// 
-        ///     Inner exception if any
-        /// 
-        public ErrorReportException InnerException { get; set; }
-
-        /// 
-        ///     Error message
-        /// 
-        public string Message { get; set; }
-
-        /// 
-        ///     Exception class name
-        /// 
-        public string Name { get; set; }
-
-        /// 
-        ///     Namespace that the exception class was defined in
-        /// 
-        public string Namespace { get; set; }
-
-        /// 
-        ///     Exception stack trace
-        /// 
-        public string StackTrace { get; set; }
-    }
+using System;
+
+namespace Coderr.Server.Domain.Core.ErrorReports
+{
+    /// 
+    ///     Exception for .
+    /// 
+    public class ErrorReportException
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        public ErrorReportException()
+        {
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// exception that this entity represents
+        /// exception
+        public ErrorReportException(Exception exception)
+        {
+            if (exception == null) throw new ArgumentNullException("exception");
+            FullName = exception.GetType().FullName;
+            Name = exception.GetType().Name;
+            Name = exception.GetType().Namespace;
+            AssemblyName = exception.GetType().Assembly.GetName().Name;
+            StackTrace = exception.StackTrace;
+            BaseClasses = exception.GetType().BaseType != null
+                ? new[] {exception.GetType().BaseType.FullName}
+                : new string[0];
+            Everything = exception.ToString();
+        }
+
+        /// 
+        ///     Assembly containing the exception
+        /// 
+        public string AssemblyName { get; set; }
+
+        /// 
+        ///     Class names of the base classes (minimum 'Exception').
+        /// 
+        public string[] BaseClasses { get; set; }
+
+        /// 
+        ///     Exception.ToString()
+        /// 
+        public string Everything { get; set; }
+
+        /// 
+        ///     Full type name
+        /// 
+        public string FullName { get; set; }
+
+        /// 
+        ///     Inner exception if any
+        /// 
+        public ErrorReportException InnerException { get; set; }
+
+        /// 
+        ///     Error message
+        /// 
+        public string Message { get; set; }
+
+        /// 
+        ///     Exception class name
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Namespace that the exception class was defined in
+        /// 
+        public string Namespace { get; set; }
+
+        /// 
+        ///     Exception stack trace
+        /// 
+        public string StackTrace { get; set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/ErrorReports/IReportsRepository.cs b/src/Server/Coderr.Server.Domain/Core/ErrorReports/IReportsRepository.cs
new file mode 100644
index 00000000..3c78b89d
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/ErrorReports/IReportsRepository.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Core.ErrorReports
+{
+    /// 
+    ///     Repository for received error reports.
+    /// 
+    public interface IReportsRepository
+    {
+
+        /// 
+        ///     Finds the by error identifier asynchronous.
+        /// 
+        /// Customer generated id (from the client library).
+        /// report if found; otherwise null.
+        /// errorId
+        Task FindByErrorIdAsync(string errorId);
+
+
+        /// 
+        ///     Get report
+        /// 
+        /// report id
+        /// report
+        /// id
+        /// no report with that id
+        Task GetAsync(int id);
+
+        /// 
+        ///     Get a list of reports
+        /// 
+        /// incidentId
+        /// Page number
+        /// items per page
+        /// Paged reports
+        /// incidentId <= 0
+        //Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/ErrorReports/ReportMapping.cs b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ReportMapping.cs
new file mode 100644
index 00000000..4bfbd376
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ReportMapping.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace Coderr.Server.Domain.Core.ErrorReports
+{
+    /// 
+    ///     Maps a received report to an incident
+    /// 
+    /// 
+    ///     
+    ///         Since we do not store complete reports for everything any more. Allows us to still keep
+    ///         track of client side generated error ids and which incident they belong to.
+    ///     
+    /// 
+    public class ReportMapping
+    {
+        public int Id { get; set; }
+
+        /// 
+        /// Incident that the report belongs to.
+        /// 
+        public int IncidentId { get; set; }
+
+        /// 
+        /// Client report id
+        /// 
+        public string ErrorId { get; set; }
+
+        /// 
+        /// Only when fetching items
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        /// When the report was generated in the client
+        /// 
+        public DateTime ReceivedAtUtc { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/ErrorReports/ReportsExtensions.cs b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ReportsExtensions.cs
new file mode 100644
index 00000000..d5db9ef2
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/ErrorReports/ReportsExtensions.cs
@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Coderr.Server.Domain.Core.Incidents;
+
+namespace Coderr.Server.Domain.Core.ErrorReports
+{
+    public static class ReportsExtensions
+    {
+        public static ErrorReportContextCollection GetCoderrCollection(
+            this IEnumerable instance)
+        {
+            return instance.FirstOrDefault(x => x.Name == "CoderrData");
+        }
+
+        public static ErrorReportContextCollection GetCoderrCollection(
+            this ErrorReportEntity instance)
+        {
+            return instance.ContextCollections.FirstOrDefault(x => x.Name == "CoderrData");
+        }
+
+
+        public static ErrorReportContextCollection FindCollection(this ErrorReportEntity instance, string collectionName)
+        {
+            return instance.ContextCollections.FirstOrDefault(x => x.Name == collectionName);
+        }
+
+        public static string FindCollectionProperty(this ErrorReportEntity instance, string collectionName, string propertyName)
+        {
+            var collection = instance.FindCollection(collectionName);
+            if (collection == null)
+            {
+                return null;
+            }
+
+            if (collection.Properties.TryGetValue(propertyName, out var value))
+            {
+                return value;
+            }
+            else
+            {
+                return null;
+            }
+        }
+    }
+}
diff --git a/src/Server/OneTrueError.App/Core/Feedback/IFeedbackRepository.cs b/src/Server/Coderr.Server.Domain/Core/Feedback/IFeedbackRepository.cs
similarity index 86%
rename from src/Server/OneTrueError.App/Core/Feedback/IFeedbackRepository.cs
rename to src/Server/Coderr.Server.Domain/Core/Feedback/IFeedbackRepository.cs
index 4e8230cb..c77aa182 100644
--- a/src/Server/OneTrueError.App/Core/Feedback/IFeedbackRepository.cs
+++ b/src/Server/Coderr.Server.Domain/Core/Feedback/IFeedbackRepository.cs
@@ -1,38 +1,39 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Threading.Tasks;
-
-namespace OneTrueError.App.Core.Feedback
-{
-    /// 
-    ///     Feedback
-    /// 
-    public interface IFeedbackRepository
-    {
-        /// 
-        ///     Find pending feedback (i.e. have not got a matching report yet)
-        /// 
-        /// reportId
-        /// entity if found; otherwise null.
-        /// reportId
-        Task FindPendingAsync(string reportId);
-
-        /// 
-        ///     Get all email addresses associated with an incident
-        /// 
-        /// incident
-        /// emails (or an emty list)
-        /// incidentId
-        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
-        Task> GetEmailAddressesAsync(int incidentId);
-
-        /// 
-        ///     Update feedback
-        /// 
-        /// entity
-        /// task
-        /// feedback
-        Task UpdateAsync(FeedbackEntity feedback);
-    }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Core.Feedback
+{
+    /// 
+    ///     Feedback
+    /// 
+    public interface IFeedbackRepository
+    {
+        /// 
+        ///     Find pending feedback (i.e. have not got a matching report yet)
+        /// 
+        /// reportId
+        /// entity if found; otherwise null.
+        /// reportId
+        Task FindPendingAsync(string reportId);
+
+        /// 
+        ///     Update feedback
+        /// 
+        /// entity
+        /// task
+        /// feedback
+        Task UpdateAsync(UserFeedback feedback);
+        
+        /// 
+        ///     Get all email addresses associated with an incident
+        /// 
+        /// incident
+        /// emails (or an emty list)
+        /// incidentId
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        Task> GetEmailAddressesAsync(int incidentId);
+
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Feedback/UserFeedback.cs b/src/Server/Coderr.Server.Domain/Core/Feedback/UserFeedback.cs
new file mode 100644
index 00000000..8d85664a
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Feedback/UserFeedback.cs
@@ -0,0 +1,87 @@
+using System;
+
+namespace Coderr.Server.Domain.Core.Feedback
+{
+    /// 
+    ///     Feedback written by the user when an exception was thrown
+    /// 
+    public class UserFeedback
+    {
+        /// 
+        ///     Application that the feedback is for
+        /// 
+        public int ApplicationId { get; private set; }
+
+        /// 
+        ///     Feedback entry can be removed.
+        /// 
+        /// 
+        ///     
+        ///         We can receive feedback before the exception have been uploaded. In those situations we need to wait on the
+        ///         error report
+        ///         before we know what incident the feedback belongs to. But since we do not want a lot of junk in our tables we
+        ///         keep
+        ///         unidentified feedback entries just for a couple of days.
+        ///     
+        /// 
+        public bool CanRemove => ApplicationId == 0 && DateTime.Now.Subtract(CreatedAtUtc).TotalDays > 5;
+
+
+        /// 
+        ///     Can only update the entry if we've been associated to a report+application.
+        /// 
+        public bool CanUpdate => ApplicationId != 0
+                                 || ReportId != 0;
+
+        /// 
+        ///     When the feebback was created by the client library
+        /// 
+        public DateTime CreatedAtUtc { get; private set; }
+
+        /// 
+        ///     Description written by the user (of what he/she did when the exception was created).
+        /// 
+        public string Description { get; private set; }
+
+        /// 
+        ///     Email address if the user want to get notified of progress.
+        /// 
+        public string EmailAddress { get; private set; }
+
+        /// 
+        ///     The unique error id that was generated by the client library
+        /// 
+        public string ErrorId { get; private set; }
+
+        /// 
+        ///     PK
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     Incident that the feedback was created for
+        /// 
+        public int IncidentId { get; private set; }
+
+        /// 
+        ///     PK for the report in our DB
+        /// 
+        public int ReportId { get; private set; }
+
+        /// 
+        ///     We've identified which report this feedback belongs to
+        /// 
+        /// Report PK, can be null if we do not store the report that the feedback came with
+        /// Incident that the report belongs to
+        /// Application that the incident belongs to
+        public void AssignToReport(int reportId, int incidentId, int applicationId)
+        {
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId");
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
+
+            ReportId = reportId;
+            IncidentId = incidentId;
+            ApplicationId = applicationId;
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Domain/Core/Incidents/EscalationState.cs b/src/Server/Coderr.Server.Domain/Core/Incidents/EscalationState.cs
new file mode 100644
index 00000000..3b1fe636
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Incidents/EscalationState.cs
@@ -0,0 +1,23 @@
+namespace Coderr.Server.Domain.Core.Incidents
+{
+    /// 
+    ///     If the incident is escalated.
+    /// 
+    public enum EscalationState
+    {
+        /// 
+        ///     Not escalated, prioritize according to the normal rules.
+        /// 
+        Normal = 0,
+
+        /// 
+        ///     Incident is important.
+        /// 
+        Important = 1,
+
+        /// 
+        ///     Error is escalated to critical.
+        /// 
+        Critical = 2
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Incidents/Events/IncidentCreated.cs b/src/Server/Coderr.Server.Domain/Core/Incidents/Events/IncidentCreated.cs
new file mode 100644
index 00000000..65bcfd68
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Incidents/Events/IncidentCreated.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace Coderr.Server.Domain.Core.Incidents.Events
+{
+    /// 
+    ///     Event for the domain (and not the report analyzer).
+    /// 
+    public class IncidentCreated
+    {
+        public IncidentCreated(int applicationId, int incidentId, string incidentName, string exceptionTypeName)
+        {
+            if (incidentName == null) throw new ArgumentNullException(nameof(incidentName));
+            if (exceptionTypeName == null) throw new ArgumentNullException(nameof(exceptionTypeName));
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId));
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId));
+            ApplicationId = applicationId;
+            IncidentId = incidentId;
+
+            var pos = incidentName.IndexOfAny(new[] { '\r', '\n' });
+            if (pos != -1)
+                incidentName = incidentName.Substring(0, pos);
+            IncidentName = incidentName;
+            ExceptionTypeName = exceptionTypeName;
+        }
+
+        protected IncidentCreated()
+        {
+        }
+
+        /// 
+        ///     Application that the incident is for
+        /// 
+
+        public int ApplicationId { get; private set; }
+
+        /// 
+        ///     Version (if reported by the client)
+        /// 
+        public string ApplicationVersion { get; set; }
+
+        public DateTime CreatedAtUtc { get; set; }
+
+        /// 
+        ///     Full name of the exception type
+        /// 
+        public string ExceptionTypeName { get; private set; }
+
+        /// 
+        ///     Incident id
+        /// 
+        public int IncidentId { get; private set; }
+
+        /// 
+        ///     Incident name
+        /// 
+        public string IncidentName { get; private set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Incidents/Events/IncidentReOpened.cs b/src/Server/Coderr.Server.Domain/Core/Incidents/Events/IncidentReOpened.cs
new file mode 100644
index 00000000..c4d7312a
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Incidents/Events/IncidentReOpened.cs
@@ -0,0 +1,59 @@
+using System;
+
+namespace Coderr.Server.Domain.Core.Incidents.Events
+{
+    /// 
+    ///     Our user had closed the incident and we just got a new report despite that.
+    /// 
+    /// 
+    ///     
+    ///         Used both in the ReportAnalyzer and in the Application.
+    ///     
+    /// 
+    public class IncidentReOpened
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// application that the incident belongs to
+        /// incident id
+        /// when the new error report was created (in the client library)
+        /// 
+        public IncidentReOpened(int applicationId, int incidentId, DateTime createdAtUtc)
+        {
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId");
+
+            ApplicationId = applicationId;
+            IncidentId = incidentId;
+            CreatedAtUtc = createdAtUtc;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected IncidentReOpened()
+        {
+        }
+
+        /// 
+        ///     Application that the report belongs to.
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Version that we reopened the incident in.
+        /// 
+        public string ApplicationVersion { get; set; }
+
+        /// 
+        ///     when the new error report was created in the client library. I.e. the report that triggered the reopening of the incident.
+        /// 
+        public DateTime CreatedAtUtc { get; set; }
+
+        /// 
+        ///     Incident that the received report belongs to.
+        /// 
+        public int IncidentId { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Incidents/IIncidentRepository.cs b/src/Server/Coderr.Server.Domain/Core/Incidents/IIncidentRepository.cs
new file mode 100644
index 00000000..181e0234
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Incidents/IIncidentRepository.cs
@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Core.Incidents
+{
+    /// 
+    ///     Incident repository
+    /// 
+    public interface IIncidentRepository
+    {
+        /// 
+        /// Delete an incident
+        /// 
+        /// id
+        /// 
+        Task Delete(int incidentId);
+
+        /// 
+        ///     Get incident
+        /// 
+        /// incident id
+        /// incident
+        /// id
+        /// No incident was found with the given key.
+        Task GetAsync(int id);
+
+        /// 
+        /// Get specified incidents
+        /// 
+        /// ids to fetch
+        /// All specified (or an exception will be thrown if any of them are missing)
+        Task> GetManyAsync(IEnumerable incidentIds);
+
+        /// 
+        ///     Count the number of incidents for the given application
+        /// 
+        /// application
+        /// total count of incidents
+        /// applicationId
+        Task GetTotalCountForAppInfoAsync(int applicationId);
+
+        /// 
+        ///     Count date for newest error.
+        /// 
+        /// application
+        /// Date for newest error
+        /// applicationId
+        Task GetLatestIncidentDate(int applicationId);
+
+        /// 
+        ///     Update incident
+        /// 
+        /// incident
+        /// task
+        /// incident
+        Task UpdateAsync(Incident incident);
+
+        /// 
+        /// Map a correlation id to an incident.
+        /// 
+        /// Incident to mark
+        /// Correlation id to associate it with
+        /// 
+        /// 
+        /// Correlation ids are used to find related incidents (they are associated with the same correlation id).
+        /// 
+        Task MapCorrelationId(int incidentId, string correlationId);
+
+        /// 
+        /// Get all specified incidents.
+        /// 
+        /// a list if ids
+        /// 
+        Task> GetAll(IEnumerable incidentIds);
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Incidents/Incident.cs b/src/Server/Coderr.Server.Domain/Core/Incidents/Incident.cs
new file mode 100644
index 00000000..c49d799d
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Incidents/Incident.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.Domain.Core.Incidents
+{
+    /// 
+    ///     Keeps track of all occurrences of a single incident (i.e. error reports which generates the same hash code)
+    /// 
+    public class Incident
+    {
+        private string _description;
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected Incident()
+        {
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// application that the incident was created for
+        /// applicationId
+        public Incident(int applicationId)
+        {
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId));
+
+            ApplicationId = applicationId;
+            CreatedAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        ///     Application that this incident belongs to
+        /// 
+        public int ApplicationId { get; private set; }
+
+        /// 
+        ///     when this incident was assigned.
+        /// 
+        public DateTime? AssignedAtUtc { get; private set; }
+
+        /// 
+        ///     The user currently working with this incident.
+        /// 
+        public int? AssignedToId { get; private set; }
+
+        /// 
+        ///     When the incident was created in the client library.
+        /// 
+        public DateTime CreatedAtUtc { get; private set; }
+
+        /// 
+        ///     Incident description.Typically first line of the exception message.
+        /// 
+        public string Description
+        {
+            get => string.IsNullOrEmpty(_description) ? "Ooops Error!" : _description;
+            set => _description = value;
+        }
+
+        /// 
+        ///     If the incident is escalated.
+        /// 
+        public EscalationState Escalation { get; set; }
+
+        /// 
+        ///     Exception type including namespace.
+        /// 
+        public string FullName { get; set; }
+
+        /// 
+        ///     PK
+        /// 
+        public int Id { get; private set; }
+
+        /// 
+        ///     Version that the error is corrected in. All inbound error reports will be ignored if they are from older
+        ///     application versions.
+        /// 
+        public string IgnoredUntilVersion { get; set; }
+
+        /// 
+        ///     When we started to ignore reports for this incident.
+        /// 
+        public DateTime IgnoringReportsSinceUtc { get; private set; }
+
+        /// 
+        ///     Person that wanted us to ignore reports.
+        /// 
+        public string IgnoringRequestedBy { get; private set; }
+
+        /// 
+        ///     Incident was marked as completed, but we've received another report for this incident
+        /// 
+        /// 
+        ///     
+        ///         Do not apply to ignored incidents.
+        ///     
+        /// 
+        [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re")]
+        public bool IsReopened { get; private set; }
+
+        /// 
+        ///     If the solution can be shared with others.
+        /// 
+        public bool IsSolutionShared { get; private set; }
+
+        /// 
+        ///     When we received the last report.
+        /// 
+        public DateTime LastReportAtUtc { get; set; }
+
+        /// 
+        ///      has been set to true, this tells when the incident was closed the last time.
+        /// 
+        public DateTime LastSolutionAtUtc { get; private set; }
+
+        /// 
+        ///     When it was reopened.
+        /// 
+        /// 
+        [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re")]
+        public DateTime ReopenedAtUtc { get; private set; }
+
+        /// 
+        ///     Number of reports that have been received so far (counts up even if the max number of reports have been received
+        ///     for this incident).
+        /// 
+        public int ReportCount { get; set; }
+
+        /// 
+        ///     Gets what was done to fix this error.
+        /// 
+        public IncidentSolution Solution { get; private set; }
+
+        /// 
+        ///     When the  was written-
+        /// 
+        public DateTime SolvedAtUtc { get; private set; }
+
+        /// 
+        ///     Stack trace from exception.
+        /// 
+        public string StackTrace { get; set; }
+
+        /// 
+        ///     Current state of this incident
+        /// 
+        public IncidentState State { get; private set; }
+
+        /// 
+        ///     When the incident was updated through the UI or when a new report was received (whatever change was made last
+        ///     time).
+        /// 
+        public DateTime UpdatedAtUtc { get; private set; }
+
+        /// 
+        ///     Assign this incident to someone.
+        /// 
+        /// User to assign to
+        public void Assign(int userId, DateTime? when = null)
+        {
+            if (userId <= 0) throw new ArgumentOutOfRangeException(nameof(userId));
+            AssignedAtUtc = DateTime.UtcNow;
+            AssignedToId = userId;
+            UpdatedAtUtc = when ?? DateTime.UtcNow;
+            State = IncidentState.Active;
+        }
+
+        /// 
+        ///     Yay! One in the dev team figured out how the error can be solved.
+        /// 
+        /// AccountId for whoever wrote the solution
+        /// Actual solution
+        /// 
+        ///     All future reports are ignored if they are reported for app versions less that the
+        ///     specified one.
+        /// 
+        /// When was the incident closed by the user?
+        /// solution
+        /// solvedBy
+        public void Close(int solvedBy, string solution, string correctedInVersion, DateTime? when = null)
+        {
+            if (solution == null) throw new ArgumentNullException(nameof(solution));
+            if (solvedBy <= 0)
+                throw new ArgumentOutOfRangeException(nameof(solvedBy), solvedBy, "Must specify a solver.");
+
+            IgnoredUntilVersion = correctedInVersion;
+            Solution = new IncidentSolution(solvedBy, solution);
+            UpdatedAtUtc = DateTime.UtcNow;
+            SolvedAtUtc = when ?? DateTime.UtcNow;
+            State = IncidentState.Closed;
+        }
+
+        /// 
+        ///     Do not want to store reports or receive notifications for this incident.
+        /// 
+        /// Name of the account.
+        /// accountName
+        public void IgnoreFutureReports(string accountName)
+        {
+            State = IncidentState.Ignored;
+            IgnoringReportsSinceUtc = DateTime.UtcNow;
+            UpdatedAtUtc = DateTime.UtcNow;
+            IgnoringRequestedBy = accountName ?? throw new ArgumentNullException(nameof(accountName));
+        }
+
+        /// 
+        ///     Dang! Got a new report after the incident being closed.
+        /// 
+        [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re")]
+        public void Reopen()
+        {
+            LastSolutionAtUtc = SolvedAtUtc;
+            State = IncidentState.Active;
+            ReopenedAtUtc = DateTime.UtcNow;
+            IsReopened = true;
+            IgnoringReportsSinceUtc = DateTime.MinValue;
+            UpdatedAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        ///     Specifies that this solution can be shared with other projects.
+        /// 
+        public void ShareSolution()
+        {
+            //TODO: Do something
+            IsSolutionShared = true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Core/Incidents/IncidentSolution.cs b/src/Server/Coderr.Server.Domain/Core/Incidents/IncidentSolution.cs
similarity index 94%
rename from src/Server/OneTrueError.App/Core/Incidents/IncidentSolution.cs
rename to src/Server/Coderr.Server.Domain/Core/Incidents/IncidentSolution.cs
index d1b34b79..d3d62c9b 100644
--- a/src/Server/OneTrueError.App/Core/Incidents/IncidentSolution.cs
+++ b/src/Server/Coderr.Server.Domain/Core/Incidents/IncidentSolution.cs
@@ -1,50 +1,50 @@
-using System;
-
-namespace OneTrueError.App.Core.Incidents
-{
-    /// 
-    ///     How the development team solved the incident.
-    /// 
-    public class IncidentSolution
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// AccountId for the user that wrote the solution
-        /// Markdown formatted solution description
-        /// createdBy
-        /// description
-        public IncidentSolution(int createdBy, string description)
-        {
-            if (description == null) throw new ArgumentNullException("description");
-            if (createdBy <= 0) throw new ArgumentOutOfRangeException("createdBy");
-
-            Description = description;
-            CreatedBy = createdBy;
-            CreatedAtUtc = DateTime.UtcNow;
-        }
-
-        /// 
-        ///     Serialization constructor.
-        /// 
-        protected IncidentSolution()
-        {
-        }
-
-        /// 
-        ///     When this solution was created
-        /// 
-        public DateTime CreatedAtUtc { get; private set; }
-
-        /// 
-        ///     AccountId for the user that wrote the solution
-        /// 
-        public int CreatedBy { get; private set; }
-
-
-        /// 
-        ///     Markdown formatted solution description
-        /// 
-        public string Description { get; private set; }
-    }
+using System;
+
+namespace Coderr.Server.Domain.Core.Incidents
+{
+    /// 
+    ///     How the development team solved the incident.
+    /// 
+    public class IncidentSolution
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// AccountId for the user that wrote the solution
+        /// Markdown formatted solution description
+        /// createdBy
+        /// description
+        public IncidentSolution(int createdBy, string description)
+        {
+            if (description == null) throw new ArgumentNullException("description");
+            if (createdBy <= 0) throw new ArgumentOutOfRangeException("createdBy");
+
+            Description = description;
+            CreatedBy = createdBy;
+            CreatedAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        ///     Serialization constructor.
+        /// 
+        protected IncidentSolution()
+        {
+        }
+
+        /// 
+        ///     When this solution was created
+        /// 
+        public DateTime CreatedAtUtc { get; private set; }
+
+        /// 
+        ///     AccountId for the user that wrote the solution
+        /// 
+        public int CreatedBy { get; private set; }
+
+
+        /// 
+        ///     Markdown formatted solution description
+        /// 
+        public string Description { get; private set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Core/Incidents/IncidentState.cs b/src/Server/Coderr.Server.Domain/Core/Incidents/IncidentState.cs
new file mode 100644
index 00000000..f11a38d2
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Core/Incidents/IncidentState.cs
@@ -0,0 +1,39 @@
+namespace Coderr.Server.Domain.Core.Incidents
+{
+    /// 
+    ///     Current state of an incident
+    /// 
+    public enum IncidentState
+    {
+        /// 
+        ///     Incident have arrived but have not yet been categorized.
+        /// 
+        New = 0,
+
+        /// 
+        ///     Incident should be fixed (assigned)
+        /// 
+        Active = 1,
+
+        /// 
+        ///     Ignore all reports for this incident
+        /// 
+        /// 
+        ///     
+        ///         All inbound reports will be discarded, no notifications will be sent to this incident and it's not show among
+        ///         new or activate incidents
+        ///     
+        /// 
+        Ignored = 2,
+
+        /// 
+        ///     Incident have been corrected.
+        /// 
+        Closed = 3,
+
+        /// 
+        /// We received a new error report on a closed incident (used in history table)
+        /// 
+        ReOpened = 4,
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Core/Users/IUserRepository.cs b/src/Server/Coderr.Server.Domain/Core/User/IUserRepository.cs
similarity index 86%
rename from src/Server/OneTrueError.App/Core/Users/IUserRepository.cs
rename to src/Server/Coderr.Server.Domain/Core/User/IUserRepository.cs
index 832af5f3..bebe4baf 100644
--- a/src/Server/OneTrueError.App/Core/Users/IUserRepository.cs
+++ b/src/Server/Coderr.Server.Domain/Core/User/IUserRepository.cs
@@ -1,40 +1,36 @@
-using System.Threading.Tasks;
-using Griffin.Data;
-
-namespace OneTrueError.App.Core.Users
-{
-    /// 
-    ///     User repository
-    /// 
-    public interface IUserRepository
-    {
-        /// 
-        ///     Create a new user
-        /// 
-        /// user
-        /// task
-        Task CreateAsync(User user);
-
-        /// 
-        ///     Find user by email
-        /// 
-        /// email address
-        /// user if found; otherwise null.
-        Task FindByEmailAsync(string emailAddress);
-
-        /// 
-        ///     Get user by account id
-        /// 
-        /// account id
-        /// user
-        /// user was not found
-        Task GetUserAsync(int accountId);
-
-        /// 
-        ///     Update user
-        /// 
-        /// user
-        /// task
-        Task UpdateAsync(User user);
-    }
-}
\ No newline at end of file
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Core.User
+{
+    public interface IUserRepository
+    {
+        /// 
+        ///     Create a new user
+        /// 
+        /// user
+        /// task
+        Task CreateAsync(User user);
+
+        /// 
+        ///     Find user by email
+        /// 
+        /// email address
+        /// user if found; otherwise null.
+        Task FindByEmailAsync(string emailAddress);
+
+        /// 
+        ///     Get user by account id
+        /// 
+        /// account id
+        /// user
+        /// user was not found
+        Task GetUserAsync(int accountId);
+
+        /// 
+        ///     Update user
+        /// 
+        /// user
+        /// task
+        Task UpdateAsync(User user);
+    }
+}
diff --git a/src/Server/OneTrueError.App/Core/Users/User.cs b/src/Server/Coderr.Server.Domain/Core/User/User.cs
similarity index 92%
rename from src/Server/OneTrueError.App/Core/Users/User.cs
rename to src/Server/Coderr.Server.Domain/Core/User/User.cs
index 6eaa0888..8036939f 100644
--- a/src/Server/OneTrueError.App/Core/Users/User.cs
+++ b/src/Server/Coderr.Server.Domain/Core/User/User.cs
@@ -1,63 +1,62 @@
-using System;
-using OneTrueError.App.Core.Accounts;
-
-namespace OneTrueError.App.Core.Users
-{
-    /// 
-    ///     User information associated to an .
-    /// 
-    public class User
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// account id
-        /// username from account object
-        /// accountId
-        /// userName
-        public User(int accountId, string userName)
-        {
-            if (userName == null) throw new ArgumentNullException("userName");
-            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
-            AccountId = accountId;
-            UserName = userName;
-        }
-
-        /// 
-        ///     Serialization constructor
-        /// 
-        protected User()
-        {
-        }
-
-        /// 
-        ///     Account id
-        /// 
-        public int AccountId { get; private set; }
-
-        /// 
-        ///     Email address
-        /// 
-        public string EmailAddress { get; set; }
-
-        /// 
-        ///     First name (if specified)
-        /// 
-        public string FirstName { get; set; }
-
-        /// 
-        ///     Last name (if specified)
-        /// 
-        public string LastName { get; set; }
-
-        /// 
-        ///     Mobile number (if specified)
-        /// 
-        public string MobileNumber { get; set; }
-
-        /// 
-        ///     userName (same as in the account object)
-        /// 
-        public string UserName { get; private set; }
-    }
+using System;
+
+namespace Coderr.Server.Domain.Core.User
+{
+    /// 
+    ///     User information associated to an .
+    /// 
+    public class User
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// account id
+        /// username from account object
+        /// accountId
+        /// userName
+        public User(int accountId, string userName)
+        {
+            if (userName == null) throw new ArgumentNullException("userName");
+            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
+            AccountId = accountId;
+            UserName = userName;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected User()
+        {
+        }
+
+        /// 
+        ///     Account id
+        /// 
+        public int AccountId { get; private set; }
+
+        /// 
+        ///     Email address
+        /// 
+        public string EmailAddress { get; set; }
+
+        /// 
+        ///     First name (if specified)
+        /// 
+        public string FirstName { get; set; }
+
+        /// 
+        ///     Last name (if specified)
+        /// 
+        public string LastName { get; set; }
+
+        /// 
+        ///     Mobile number (if specified)
+        /// 
+        public string MobileNumber { get; set; }
+
+        /// 
+        ///     userName (same as in the account object)
+        /// 
+        public string UserName { get; private set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/EntityNotFoundException.cs b/src/Server/Coderr.Server.Domain/EntityNotFoundException.cs
new file mode 100644
index 00000000..019e91a4
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/EntityNotFoundException.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Coderr.Server.Domain
+{
+    public class EntityNotFoundException : Exception
+    {
+        public EntityNotFoundException(string message) : base(message)
+        {
+
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/ApplicationVersion.cs b/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/ApplicationVersion.cs
new file mode 100644
index 00000000..9527eba8
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/ApplicationVersion.cs
@@ -0,0 +1,75 @@
+using System;
+
+namespace Coderr.Server.Domain.Modules.ApplicationVersions
+{
+    /// 
+    ///     An version that we track.
+    /// 
+    public class ApplicationVersion
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// FK
+        /// Name of the application
+        /// Version (x.x.x.x)
+        /// 
+        /// 
+        public ApplicationVersion(int applicationId, string applicationName, string version)
+        {
+            if (applicationName == null) throw new ArgumentNullException("applicationName");
+            if (version == null) throw new ArgumentNullException("version");
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
+            ApplicationId = applicationId;
+            ApplicationName = applicationName;
+            Version = version;
+            ReceivedFirstReportAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        /// Serialization constructor
+        /// 
+        protected ApplicationVersion()
+        {
+            
+        }
+
+        /// 
+        ///     Id of the application that this version is for
+        /// 
+        public int ApplicationId { get; private set; }
+
+        /// 
+        ///     Name of the application that this version is for
+        /// 
+        public string ApplicationName { get; private set; }
+
+        /// 
+        ///     ID of this entity
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     When we received the first report for this version
+        /// 
+        public DateTime ReceivedFirstReportAtUtc { get; private set; }
+
+        /// 
+        ///     When we received the last report for this version
+        /// 
+        public DateTime ReceivedLastReportAtUtc { get; private set; }
+
+        /// 
+        ///     Assembly version (x.x.x.x)
+        /// 
+        public string Version { get; private set; }
+
+        /// 
+        ///     Update the date that tracks the last time we received a report
+        /// 
+        public void UpdateReportDate()
+        {
+            ReceivedLastReportAtUtc = DateTime.UtcNow;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/ApplicationVersionMonth.cs b/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/ApplicationVersionMonth.cs
new file mode 100644
index 00000000..cf7c7fcd
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/ApplicationVersionMonth.cs
@@ -0,0 +1,81 @@
+using System;
+
+namespace Coderr.Server.Domain.Modules.ApplicationVersions
+{
+    /// 
+    ///     Tracks number of incidents/reports for a specific year/month
+    /// 
+    public class ApplicationVersionMonth
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Id from 
+        /// Which year/Month this entry tracks
+        public ApplicationVersionMonth(int versionId, DateTime yearMonth)
+        {
+            if (versionId <= 0) throw new ArgumentOutOfRangeException("versionId");
+            if (yearMonth.Day != 1)
+                throw new ArgumentException("Day must be set to 1", "yearMonth");
+
+            VersionId = versionId;
+            YearMonth = yearMonth;
+        }
+
+        /// 
+        /// Serialization constructor
+        /// 
+        protected ApplicationVersionMonth()
+        {
+            
+        }
+
+        /// 
+        ///     Id for this year/month entry
+        /// 
+        public int Id { get; private set; }
+
+        /// 
+        ///     Number of new incidents this month
+        /// 
+        public int IncidentCount { get; private set; }
+
+        /// 
+        ///     When we received a report
+        /// 
+        public DateTime LastUpdateAtUtc { get; set; }
+
+        /// 
+        ///     Number of reports this month
+        /// 
+        public int ReportCount { get; private set; }
+
+        /// 
+        ///     FK to .
+        /// 
+        public int VersionId { get; set; }
+
+        /// 
+        ///     Which year/month this is for
+        /// 
+        public DateTime YearMonth { get; private set; }
+
+        /// 
+        ///     Increase incident count
+        /// 
+        public void IncreaseIncidentCount()
+        {
+            IncidentCount++;
+            LastUpdateAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        ///     Increase report count
+        /// 
+        public void IncreaseReportCount()
+        {
+            ReportCount++;
+            LastUpdateAtUtc = DateTime.UtcNow;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/IApplicationVersionRepository.cs b/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/IApplicationVersionRepository.cs
new file mode 100644
index 00000000..af7f4b23
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/ApplicationVersions/IApplicationVersionRepository.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Modules.ApplicationVersions
+{
+    public interface IApplicationVersionRepository
+    {
+        /// 
+        ///     Create statistics information for version usage.
+        /// 
+        /// info
+        /// task
+        /// month
+        /// Failed to insert row
+        Task CreateAsync(ApplicationVersionMonth month);
+
+        /// 
+        ///     Create a new application version
+        /// 
+        /// entity
+        /// task
+        /// month
+        /// Failed to insert row
+        Task CreateAsync(ApplicationVersion entity);
+
+        /// 
+        ///     Get version
+        /// 
+        /// Incident to get versions for
+        /// version if found; otherwise null
+        /// applicationId;version
+        /// Failed to query DB
+        Task> FindForIncidentAsync(int incidentId);
+
+        /// 
+        ///     Get monthly exception report
+        /// 
+        /// Application id
+        /// Year, four digits
+        /// Month
+        /// entity if found; otherwise null.
+        /// applicationId
+        /// year;month
+        /// Failed to query DB
+        Task FindMonthForApplicationAsync(int applicationId, int year, int month);
+
+        /// 
+        ///     Get version
+        /// 
+        /// id for the application that we want to fetch a version for
+        /// Version ("1.0.0")
+        /// version if found; otherwise null
+        /// applicationId;version
+        /// Failed to query DB
+        Task FindVersionAsync(int applicationId, string version);
+
+        /// 
+        ///     Find all versions that we've received error reports for.
+        /// 
+        /// application id
+        /// versions
+        Task> FindVersionsAsync(int appId);
+
+        /// 
+        ///     Save version (ignore if it have already been stored).
+        /// 
+        /// incident to attach version to
+        /// Id for the application version
+        void SaveIncidentVersion(int incidentId, int versionId);
+
+
+        /// 
+        ///     Update an existing monthly report
+        /// 
+        /// entity
+        /// 
+        /// month
+        /// Failed to execute SQL
+        Task UpdateAsync(ApplicationVersionMonth month);
+
+        /// 
+        ///     Update an existing version info
+        /// 
+        /// version info
+        /// 
+        /// entity
+        /// Failed to query DB
+        Task UpdateAsync(ApplicationVersion entity);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/ErrorOrigins/ErrorOrigin.cs b/src/Server/Coderr.Server.Domain/Modules/ErrorOrigins/ErrorOrigin.cs
new file mode 100644
index 00000000..05a12d5f
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/ErrorOrigins/ErrorOrigin.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.Domain.Modules.ErrorOrigins
+{
+    /// 
+    ///     Geographic location of where an error report originated from.
+    /// 
+    public class ErrorOrigin
+    {
+        public const double EmptyLatitude = 91;
+        public const double EmptyLongitude = 181;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// IP address that we received the report from
+        /// Longitude that the IP lookup service returned.
+        /// Latitude that the IP lookup service returned.
+        public ErrorOrigin(string ipAddress, double longitude, double latitude)
+        {
+            if (string.IsNullOrEmpty(ipAddress) && longitude <= 0 && latitude <= 0)
+                throw new ArgumentException("Either IPAddress or long/lat must be specified.");
+
+            IpAddress = ipAddress;
+            Longitude = longitude;
+            Latitude = latitude;
+        }
+
+        public ErrorOrigin(string ipAddress)
+        {
+            IpAddress = ipAddress ?? throw new ArgumentNullException(nameof(ipAddress));
+            Longitude = EmptyLongitude;
+            Latitude = EmptyLatitude;
+        }
+
+        /// 
+        ///     For the mapper.
+        /// 
+        protected ErrorOrigin()
+        {
+        }
+
+        /// 
+        ///     City reported by the lookup service.
+        /// 
+        public string City { get; set; }
+
+        /// 
+        ///     Country code (top domain)
+        /// 
+        public string CountryCode { get; set; }
+
+
+        /// 
+        ///     Name of country.
+        /// 
+        public string CountryName { get; set; }
+
+        public int Id { get; set; }
+
+        /// 
+        ///     IP address that we received the report from
+        /// 
+        [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ip")]
+        public string IpAddress { get; private set; }
+
+        /// Longitude that the IP lookup service returned.
+        /// 
+        ///     91 if not specified
+        /// 
+        public double Latitude { get; set; }
+
+        /// 
+        ///     Latitude that the IP lookup service returned.
+        /// 
+        /// 
+        ///     
+        ///         181 if not specified.
+        ///     
+        /// 
+        public double Longitude { get; set; }
+
+        /// 
+        ///     TODO: WTF IS THIS?!
+        /// 
+        public string RegionCode { get; set; }
+
+        /// 
+        ///     Country name
+        /// 
+        public string RegionName { get; set; }
+
+        /// 
+        ///     Zip / postal code.
+        /// 
+        public string ZipCode { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/History/HistoryEntry.cs b/src/Server/Coderr.Server.Domain/Modules/History/HistoryEntry.cs
new file mode 100644
index 00000000..d6b8bdee
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/History/HistoryEntry.cs
@@ -0,0 +1,51 @@
+using System;
+using System.ComponentModel;
+using Coderr.Server.Domain.Core.Incidents;
+
+namespace Coderr.Server.Domain.Modules.History
+{
+    public class HistoryEntry
+    {
+        public HistoryEntry(int incidentId, int? accountId, IncidentState state)
+        {
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId));
+            if (!Enum.IsDefined(typeof(IncidentState), state))
+                throw new InvalidEnumArgumentException(nameof(state), (int) state, typeof(IncidentState));
+            if (accountId != null && accountId <= 0)
+                throw new ArgumentOutOfRangeException("AccountId should either be unspecified (system account) or larger than 0.");
+
+            IncidentId = incidentId;
+            AccountId = accountId;
+            IncidentState = state;
+            CreatedAtUtc = DateTime.UtcNow;
+        }
+
+        protected HistoryEntry()
+        {
+        }
+
+        /// 
+        ///     User that made the transition to the new state
+        /// 
+        /// 
+        ///     
+        ///         null if the system made the transition
+        ///     
+        /// 
+        public int? AccountId { get; private set; }
+
+        /// 
+        ///     Which version that the change was made in.
+        /// 
+        public string ApplicationVersion { get; set; }
+
+        /// 
+        ///     when the incident changed to this state
+        /// 
+        public DateTime CreatedAtUtc { get; private set; }
+
+        public int Id { get; set; }
+        public int IncidentId { get; private set; }
+        public IncidentState IncidentState { get; private set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/History/IHistoryRepository.cs b/src/Server/Coderr.Server.Domain/Modules/History/IHistoryRepository.cs
new file mode 100644
index 00000000..2c9cd162
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/History/IHistoryRepository.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Modules.History
+{
+    public interface IHistoryRepository
+    {
+        Task CreateAsync(HistoryEntry entry);
+        Task> GetByIncidentId(int incidentId);
+    }
+}
diff --git a/src/Server/Coderr.Server.Domain/Modules/Logs/ILogsRepository.cs b/src/Server/Coderr.Server.Domain/Modules/Logs/ILogsRepository.cs
new file mode 100644
index 00000000..01fefb24
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/Logs/ILogsRepository.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Modules.Logs
+{
+    public interface ILogsRepository
+    {
+        Task Exists(int incidentId, int? reportId);
+        Task> Get(int incidentId, int? reportId);
+        Task Create(int incidentId, int reportId, IReadOnlyList entries);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/Logs/LogEntry.cs b/src/Server/Coderr.Server.Domain/Modules/Logs/LogEntry.cs
new file mode 100644
index 00000000..6375db5a
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/Logs/LogEntry.cs
@@ -0,0 +1,20 @@
+using System;
+
+namespace Coderr.Server.Domain.Modules.Logs
+{
+    public class LogEntry
+    {
+        public DateTime TimeStampUtc { get; set; }
+
+        public string Message { get; set; }
+
+        public LogLevel Level { get; set; }
+
+        public string Exception { get; set; }
+
+        /// 
+        /// Class name, method or similar (i.e. where in the code that this log entry comes from)
+        /// 
+        public string Source { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/Logs/LogLevel.cs b/src/Server/Coderr.Server.Domain/Modules/Logs/LogLevel.cs
new file mode 100644
index 00000000..a5cbeb87
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/Logs/LogLevel.cs
@@ -0,0 +1,13 @@
+namespace Coderr.Server.Domain.Modules.Logs
+{
+    public enum LogLevel
+    {
+        Trace = 1,
+        Debug = 2,
+        Info = 3,
+        Warning = 4,
+        Error = 5,
+        Critical = 6
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/ReportSpikes/ErrorReportSpike.cs b/src/Server/Coderr.Server.Domain/Modules/ReportSpikes/ErrorReportSpike.cs
similarity index 93%
rename from src/Server/OneTrueError.App/Modules/ReportSpikes/ErrorReportSpike.cs
rename to src/Server/Coderr.Server.Domain/Modules/ReportSpikes/ErrorReportSpike.cs
index 329b5319..f680ba1d 100644
--- a/src/Server/OneTrueError.App/Modules/ReportSpikes/ErrorReportSpike.cs
+++ b/src/Server/Coderr.Server.Domain/Modules/ReportSpikes/ErrorReportSpike.cs
@@ -1,85 +1,93 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-
-namespace OneTrueError.App.Modules.ReportSpikes
-{
-    /// 
-    ///     A spike is when we receive an unusual amount of reports during a short period of time for an application.
-    /// 
-    public class ErrorReportSpike
-    {
-        /// 
-        ///     Creates a new instance of 
-        /// 
-        /// Application that the spike was detected for
-        /// Initial count when the spike was detetced
-        public ErrorReportSpike(int applicationId, int count)
-        {
-            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
-            if (count <= 0) throw new ArgumentOutOfRangeException("count");
-
-            ApplicationId = applicationId;
-            Count = count;
-            SpikeDate = DateTime.Today;
-            NotifiedAccounts = new int[0];
-        }
-
-        /// 
-        ///     Application that the inspected incident belongs to.
-        /// 
-        public int ApplicationId { get; private set; }
-
-        /// 
-        ///     Number of error reports
-        /// 
-        public int Count { get; private set; }
-
-        /// 
-        ///     Accounts that we've sent notifications to for this spike.
-        /// 
-        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays",
-            Justification = "I like my arrays.")]
-        public int[] NotifiedAccounts { get; private set; }
-
-        /// 
-        ///     Date (and not DateTime) for the date when the spike was detected.
-        /// 
-        /// 
-        ///     the purpose is to make sure that we do not send out several spike messages for the same spike
-        /// 
-        public DateTime SpikeDate { get; private set; }
-
-        /// 
-        ///     Add an account that has been notified
-        /// 
-        /// account id
-        public void AddNotifiedAccount(int accountId)
-        {
-            if (HasAccount(accountId))
-                throw new InvalidOperationException("Account have already been notified: " + accountId);
-            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
-
-            NotifiedAccounts = NotifiedAccounts.Concat(new[] {accountId}).ToArray();
-        }
-
-        /// 
-        ///     Check if the given account id have been previously notified for this spike.
-        /// 
-        /// 
-        /// 
-        public bool HasAccount(int accountId)
-        {
-            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
-            return NotifiedAccounts.Contains(accountId);
-        }
-
-        /// 
-        ///     Increase the number of reports that we have received since the spike was detected.
-        /// 
-        public void IncreaseReportCount()
-        {
-            Count += 1;
-        }
-    }
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Coderr.Server.Domain.Modules.ReportSpikes
+{
+    /// 
+    ///     A spike is when we receive an unusual amount of reports during a short period of time for an application.
+    /// 
+    [Obsolete("Use SpikeAggregation instead.")]
+    public class ErrorReportSpike
+    {
+        /// 
+        ///     Creates a new instance of 
+        /// 
+        /// Application that the spike was detected for
+        /// Initial count when the spike was detected
+        public ErrorReportSpike(int applicationId, int count)
+        {
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
+            if (count <= 0) throw new ArgumentOutOfRangeException("count");
+
+            ApplicationId = applicationId;
+            Count = count;
+            SpikeDate = DateTime.Today;
+            NotifiedAccounts = new int[0];
+        }
+
+        protected ErrorReportSpike()
+        {
+            
+        }
+
+        public int Id { get; set; }
+
+        /// 
+        ///     Application that the inspected incident belongs to.
+        /// 
+        public int ApplicationId { get; private set; }
+
+        /// 
+        ///     Number of error reports
+        /// 
+        public int Count { get; private set; }
+
+        /// 
+        ///     Accounts that we've sent notifications to for this spike.
+        /// 
+        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays",
+            Justification = "I like my arrays.")]
+        public int[] NotifiedAccounts { get; private set; }
+
+        /// 
+        ///     Date (and not DateTime) for the date when the spike was detected.
+        /// 
+        /// 
+        ///     the purpose is to make sure that we do not send out several spike messages for the same spike
+        /// 
+        public DateTime SpikeDate { get; private set; }
+
+        /// 
+        ///     Add an account that has been notified
+        /// 
+        /// account id
+        public void AddNotifiedAccount(int accountId)
+        {
+            if (HasAccount(accountId))
+                throw new InvalidOperationException("Account have already been notified: " + accountId);
+            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
+
+            NotifiedAccounts = NotifiedAccounts.Concat(new[] {accountId}).ToArray();
+        }
+
+        /// 
+        ///     Check if the given account id have been previously notified for this spike.
+        /// 
+        /// 
+        /// 
+        public bool HasAccount(int accountId)
+        {
+            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
+            return NotifiedAccounts.Contains(accountId);
+        }
+
+        /// 
+        ///     Increase the number of reports that we have received since the spike was detected.
+        /// 
+        public void IncreaseReportCount()
+        {
+            Count += 1;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/Similarities/SimilaritiesReport.cs b/src/Server/Coderr.Server.Domain/Modules/Similarities/SimilaritiesReport.cs
new file mode 100644
index 00000000..88e0ab3d
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/Similarities/SimilaritiesReport.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Coderr.Server.Domain.Modules.Similarities
+{
+    /// 
+    ///     Stores information about all context collection properties and how often their different values are the same.
+    /// 
+    /// 
+    ///     For instance. 90 reports may have v4.0.0 of the assembly Framework.Core while 10 reports have v3.5.0. that means
+    ///     that v4.0.0 is used in 90% of the reports and therefore the
+    ///     assembly that the support team should use when trying to find the exception.
+    /// 
+    public class SimilaritiesReport
+    {
+        private static readonly string[] IgnoredCollections = {"OpenForms", "Screenshots"};
+        private readonly List _collections = new List();
+
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Incident that this report belongs to
+        /// incidentId
+        public SimilaritiesReport(int incidentId)
+        {
+            if (incidentId < 1) throw new ArgumentNullException("incidentId");
+            IncidentId = incidentId;
+            ReportCount = 0;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected SimilaritiesReport()
+        {
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Incident that this report belongs to
+        /// all generated collections
+        /// collections
+        /// incidentId
+        [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")]
+        public SimilaritiesReport(int incidentId, List collections)
+        {
+            if (collections == null) throw new ArgumentNullException("collections");
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId");
+            IncidentId = incidentId;
+            _collections = collections;
+        }
+
+        /// 
+        ///     Collections
+        /// 
+        public IEnumerable Collections
+        {
+            get { return _collections; }
+        }
+
+        /// 
+        ///     Incident that this is a analysis for.
+        /// 
+        public int IncidentId { get; private set; }
+
+        /// 
+        ///     Number of reports for the incident
+        /// 
+        public int ReportCount { get; private set; }
+
+        /// 
+        ///     Get a specific collection
+        /// 
+        /// context collection name
+        /// collection if found; otherwise null.
+        protected SimilarityCollection GetCollection(string contextName)
+        {
+            if (contextName == null) throw new ArgumentNullException("contextName");
+
+            return _collections.FirstOrDefault(x => x.Name.Equals(contextName, StringComparison.OrdinalIgnoreCase));
+        }
+
+        public void AddSimilarity(string contextName, string propertyName, object adaptedValue)
+        {
+            var collection = GetCollection(contextName);
+            if (collection == null)
+            {
+                collection = new SimilarityCollection(IncidentId, contextName);
+                _collections.Add(collection);
+            }
+
+            collection.Add(propertyName, adaptedValue);
+        }
+
+        public void IncreateReportCount()
+        {
+            ReportCount += 1;
+        }
+
+        public bool IsIgnored(string contextName)
+        {
+            return IgnoredCollections.Contains(contextName, StringComparer.OrdinalIgnoreCase);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Similarity.cs b/src/Server/Coderr.Server.Domain/Modules/Similarities/Similarity.cs
similarity index 79%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Similarity.cs
rename to src/Server/Coderr.Server.Domain/Modules/Similarities/Similarity.cs
index 782e828e..174f2a66 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Similarity.cs
+++ b/src/Server/Coderr.Server.Domain/Modules/Similarities/Similarity.cs
@@ -1,162 +1,156 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-
-namespace OneTrueError.App.Modules.Similarities.Domain
-{
-    /// 
-    ///     Information about a specific context collection property with all if it's values.
-    /// 
-    /// 
-    ///     
-    ///         No support for null values internally, so they are converted into empty strings. Hence any similarity
-    ///         with an empty string might have been null too.
-    ///     
-    /// 
-    public class Similarity
-    {
-        //do not set as readonly, destroys serialization
-        // ReSharper disable once FieldCanBeMadeReadOnly.Local
-        private IDictionary _similarityValues;
-
-        /// 
-        ///     Initializes a new instance of the  class.
-        /// 
-        /// Name of the property.
-        /// 
-        ///     contextCollectionName
-        ///     or
-        ///     propertyName
-        ///     or
-        ///     value
-        /// 
-        public Similarity(string propertyName)
-        {
-            if (propertyName == null) throw new ArgumentNullException("propertyName");
-
-            PropertyName = propertyName;
-            ValueCount = 0;
-            _similarityValues = new Dictionary();
-        }
-
-        /// 
-        ///     Serialization constructor
-        /// 
-        protected Similarity()
-        {
-        }
-
-        /// 
-        ///     Id of .
-        /// 
-        public int CollectionId { get; set; }
-
-        /// 
-        ///     Similarity id
-        /// 
-        public int Id { get; set; }
-
-        /// 
-        ///     Name of the property
-        /// 
-        public string PropertyName { get; private set; }
-
-        /// 
-        ///     Amount of added values (i.e. total number of times we've added a count to a value).
-        /// 
-        public int ValueCount { get; set; }
-
-        /// 
-        ///     All values which have been collected for this similarity (i.e. context collection property)
-        /// 
-        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
-        public IEnumerable Values
-        {
-            get { return _similarityValues.Values; }
-            // ReSharper disable once UnusedMember.Local ==> used by serialization.
-            private set { _similarityValues = value.ToDictionary(x => x.Value); }
-        }
-
-        /// 
-        ///     Adds a similarity value, if the similarity value exists its count is incremented.
-        /// 
-        /// Value may be null
-        public void AddValue(string value)
-        {
-            if (value == null)
-                value = "";
-
-            SimilarityValue similarityValue;
-            if (!_similarityValues.TryGetValue(value, out similarityValue))
-            {
-                similarityValue = new SimilarityValue(value);
-                _similarityValues.Add(value, similarityValue);
-            }
-
-            ValueCount += 1;
-            similarityValue.IncreaseUsage(ValueCount);
-
-            foreach (var value1 in _similarityValues)
-            {
-                value1.Value.Recalculate(ValueCount);
-            }
-        }
-
-        /// 
-        ///     Invoked during fetching.
-        /// 
-        /// The value.
-        public void AddValue(SimilarityValue value)
-        {
-            if (value == null) throw new ArgumentNullException("value");
-
-            _similarityValues.Add(value.Value, value);
-        }
-
-        /// 
-        ///     Get the value with highest percentage count.
-        /// 
-        /// Value
-        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
-        public SimilarityValue GetMostFrequentlyUsedValue()
-        {
-            return _similarityValues.Values
-                .OrderByDescending(x => x.Percentage)
-                .First();
-        }
-
-        /// 
-        ///     Invoked during fetch (i.e. repository load)
-        /// 
-        /// 
-        public void LoadValues(SimilarityValue[] values)
-        {
-            if (values == null) throw new ArgumentNullException("values");
-
-            foreach (var value in values)
-            {
-                //TODO: Find out why this bug occur
-                // i.e. sometimes the exact same value is repeated over multiple values.
-                if (_similarityValues.ContainsKey(value.Value))
-                    _similarityValues[value.Value].IncreaseUsage(ValueCount);
-                else
-                    _similarityValues.Add(value.Value, value);
-            }
-            if (ValueCount == 0)
-                ValueCount = _similarityValues.Sum(x => x.Value.Count);
-        }
-
-        /// 
-        ///     Returns a string that represents the current object.
-        /// 
-        /// 
-        ///     A string that represents the current object.
-        /// 
-        /// 2
-        public override string ToString()
-        {
-            return string.Format("{0}[{1}]", PropertyName, string.Join(", ", Values.Select(x => x.Value)));
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+
+namespace Coderr.Server.Domain.Modules.Similarities
+{
+    /// 
+    ///     Information about a specific context collection property with all if it's values.
+    /// 
+    /// 
+    ///     
+    ///         No support for null values internally, so they are converted into empty strings. Hence any similarity
+    ///         with an empty string might have been null too.
+    ///     
+    /// 
+    public class Similarity
+    {
+        //do not set as readonly, destroys serialization
+        // ReSharper disable once FieldCanBeMadeReadOnly.Local
+        private IDictionary _similarityValues;
+
+        /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        /// Name of the property.
+        /// 
+        ///     contextCollectionName
+        ///     or
+        ///     propertyName
+        ///     or
+        ///     value
+        /// 
+        public Similarity(string propertyName)
+        {
+            if (propertyName == null) throw new ArgumentNullException("propertyName");
+
+            PropertyName = propertyName;
+            ValueCount = 1;
+            _similarityValues = new Dictionary();
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected Similarity()
+        {
+        }
+
+        /// 
+        ///     Id of .
+        /// 
+        public int CollectionId { get; set; }
+
+        /// 
+        ///     Similarity id
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     Name of the property
+        /// 
+        public string PropertyName { get; private set; }
+
+        /// 
+        ///     Amount of added values (i.e. total number of times we've added a count to a value).
+        /// 
+        public int ValueCount { get; set; }
+
+        /// 
+        ///     All values which have been collected for this similarity (i.e. context collection property)
+        /// 
+        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+        public IEnumerable Values
+        {
+            get { return _similarityValues.Values; }
+            // ReSharper disable once UnusedMember.Local ==> used by serialization.
+            private set { _similarityValues = value.ToDictionary(x => x.Value); }
+        }
+
+        /// 
+        ///     Adds a similarity value, if the similarity value exists its count is incremented.
+        /// 
+        /// Value may be null
+        public void AddValue(string value)
+        {
+            if (value == null)
+                value = "";
+
+            if (!_similarityValues.TryGetValue(value, out var similarityValue))
+            {
+                similarityValue = new SimilarityValue(value);
+                _similarityValues.Add(value, similarityValue);
+            }
+
+            ValueCount += 1;
+            similarityValue.IncreaseUsage(ValueCount);
+
+            foreach (var value1 in _similarityValues)
+            {
+                value1.Value.Recalculate(ValueCount);
+            }
+        }
+
+
+        /// 
+        ///     Get the value with highest percentage count.
+        /// 
+        /// Value
+        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
+        public SimilarityValue GetMostFrequentlyUsedValue()
+        {
+            return _similarityValues.Values
+                .OrderByDescending(x => x.Percentage)
+                .First();
+        }
+
+        /// 
+        ///     Invoked during fetch (i.e. repository load)
+        /// 
+        /// 
+        public void LoadValues(SimilarityValue[] values)
+        {
+            if (values == null) throw new ArgumentNullException("values");
+
+
+            // Calculate the total.
+            // Must do this first since we are empty
+            // due to being loaded from the DB
+            ValueCount = values.Sum(x => x.Count);
+
+            foreach (var value in values)
+            {
+                if (_similarityValues.TryGetValue(value.Value, out var similarityValue))
+                    similarityValue.IncreaseUsage(ValueCount);
+                else
+                {
+                    _similarityValues.Add(value.Value, value);
+                    value.Recalculate(ValueCount);
+                }
+            }
+        }
+
+        /// 
+        ///     Returns a string that represents the current object.
+        /// 
+        /// 
+        ///     A string that represents the current object.
+        /// 
+        /// 2
+        public override string ToString()
+        {
+            return $"{PropertyName}[{string.Join(", ", Values.Select(x => x.Value))}]";
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilarityCollection.cs b/src/Server/Coderr.Server.Domain/Modules/Similarities/SimilarityCollection.cs
similarity index 82%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilarityCollection.cs
rename to src/Server/Coderr.Server.Domain/Modules/Similarities/SimilarityCollection.cs
index 36d8b02c..75f6eef1 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilarityCollection.cs
+++ b/src/Server/Coderr.Server.Domain/Modules/Similarities/SimilarityCollection.cs
@@ -1,137 +1,135 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using OneTrueError.Api.Core.Reports;
-
-namespace OneTrueError.App.Modules.Similarities.Domain
-{
-    /// 
-    ///     A collection corresponding to , but where value usage have been analysed.
-    /// 
-    [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix",
-        Justification = "Namespace is named 'Similarities'.")]
-    public class SimilarityCollection
-    {
-        private readonly List _items = new List();
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// Incident that the collection belongs to
-        /// Name of the context collection that this is a analysis for.
-        /// contextName
-        /// incidentId
-        public SimilarityCollection(int incidentId, string contextName)
-        {
-            if (contextName == null) throw new ArgumentNullException("contextName");
-            if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId");
-            IncidentId = incidentId;
-            Name = contextName;
-        }
-
-        /// 
-        ///     Serialization constructor
-        /// 
-        protected SimilarityCollection()
-        {
-        }
-
-        /// 
-        ///     Similarity collection identity
-        /// 
-        public int Id { get; private set; }
-
-        /// 
-        ///     Incident that this collection belongs to
-        /// 
-        public int IncidentId { get; private set; }
-
-        /// 
-        ///     Name of the context collection
-        /// 
-        public string Name { get; private set; }
-
-        /// 
-        ///     All analysed properties
-        /// 
-        public IList Properties
-        {
-            get { return _items; }
-        }
-
-        /// 
-        ///     Add a new property value
-        /// 
-        /// Property name, same as in the context collection that we are analysing
-        /// normalized value
-        public void Add(string propertyName, object adaptedValue)
-        {
-            if (propertyName == null) throw new ArgumentNullException("propertyName");
-            if (adaptedValue == null) throw new ArgumentNullException("adaptedValue");
-
-            var item =
-                _items.FirstOrDefault(
-                    x => x.PropertyName.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
-            if (item == null)
-            {
-                item = new Similarity(propertyName);
-                _items.Add(item);
-            }
-
-            item.AddValue(adaptedValue.ToString());
-        }
-
-        /// 
-        ///     Add a new similarity
-        /// 
-        /// similarity
-        /// similarity
-        public void Add(Similarity similarity)
-        {
-            if (similarity == null) throw new ArgumentNullException("similarity");
-            _items.Add(similarity);
-        }
-
-        /// 
-        ///     Returns a string that represents the current object.
-        /// 
-        /// 
-        ///     Context name
-        /// 
-        /// 2
-        public override string ToString()
-        {
-            return Name;
-        }
-
-        /// 
-        ///     For weak dirty hacking data layer
-        /// 
-        /// PK
-        internal void SetId(int id)
-        {
-            Id = id;
-        }
-    }
-}
-
-/*CREATE TABLE [dbo].[Similarities] (
-    Id int identity NOT NULL primary key,
-    CollectionId int not null,
-	Name varchar(40) not null,
-	MostCommonValue varchar(40) not null,
-	MostCommonValuePercentage smallint not null,
-	ValueCount int not null,
-);
-
-
-CREATE TABLE [dbo].[SimilarityValues] (
-    Id int identity NOT NULL primary key,
-    SimilarityId int not null,
-	Name varchar(40) not null,
-	Count int not null,
-	Percentage int not null,
-	Value nvarchar(max) not null,
-);
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.Domain.Modules.Similarities
+{
+    /// 
+    ///     A collection corresponding to , but where value usage have been analyzed.
+    /// 
+    [SuppressMessage("Microsoft.Naming", "CA1711:IdentifiersShouldNotHaveIncorrectSuffix",
+        Justification = "Namespace is named 'Similarities'.")]
+    public class SimilarityCollection
+    {
+        private readonly List _items = new List();
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Incident that the collection belongs to
+        /// Name of the context collection that this is a analysis for.
+        /// contextName
+        /// incidentId
+        public SimilarityCollection(int incidentId, string contextName)
+        {
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId));
+            IncidentId = incidentId;
+            Name = contextName ?? throw new ArgumentNullException(nameof(contextName));
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected SimilarityCollection()
+        {
+        }
+
+        /// 
+        ///     Similarity collection identity
+        /// 
+        public int Id { get; private set; }
+
+        /// 
+        ///     Incident that this collection belongs to
+        /// 
+        public int IncidentId { get; private set; }
+
+        /// 
+        ///     Name of the context collection
+        /// 
+        public string Name { get; private set; }
+
+        /// 
+        ///     All analysed properties
+        /// 
+        public IList Properties
+        {
+            get { return _items; }
+        }
+
+        /// 
+        ///     Add a new property value
+        /// 
+        /// Property name, same as in the context collection that we are analysing
+        /// normalized value
+        public void Add(string propertyName, object adaptedValue)
+        {
+            if (propertyName == null) throw new ArgumentNullException("propertyName");
+            if (adaptedValue == null) throw new ArgumentNullException("adaptedValue");
+
+            Similarity item = null;
+
+            // ReSharper disable once ForCanBeConvertedToForeach
+            // ReSharper disable once LoopCanBeConvertedToQuery
+            for (var i = 0; i < _items.Count; i++)
+            {
+                if (_items[i].PropertyName != propertyName)
+                    continue;
+
+                item = _items[i];
+                break;
+            }
+
+            if (item == null)
+            {
+                item = new Similarity(propertyName);
+                _items.Add(item);
+            }
+
+            item.AddValue(adaptedValue.ToString());
+        }
+
+        /// 
+        ///     Add a new similarity
+        /// 
+        /// similarity
+        /// similarity
+        public void Add(Similarity similarity)
+        {
+            if (similarity == null) throw new ArgumentNullException("similarity");
+            _items.Add(similarity);
+        }
+
+        /// 
+        ///     Returns a string that represents the current object.
+        /// 
+        /// 
+        ///     Context name
+        /// 
+        /// 2
+        public override string ToString()
+        {
+            return Name;
+        }
+    }
+}
+
+/*CREATE TABLE [dbo].[Similarities] (
+    Id int identity NOT NULL primary key,
+    CollectionId int not null,
+	Name varchar(40) not null,
+	MostCommonValue varchar(40) not null,
+	MostCommonValuePercentage smallint not null,
+	ValueCount int not null,
+);
+
+
+CREATE TABLE [dbo].[SimilarityValues] (
+    Id int identity NOT NULL primary key,
+    SimilarityId int not null,
+	Name varchar(40) not null,
+	Count int not null,
+	Percentage int not null,
+	Value nvarchar(max) not null,
+);
 */
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilarityValue.cs b/src/Server/Coderr.Server.Domain/Modules/Similarities/SimilarityValue.cs
similarity index 94%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilarityValue.cs
rename to src/Server/Coderr.Server.Domain/Modules/Similarities/SimilarityValue.cs
index 358b98b1..68dad550 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilarityValue.cs
+++ b/src/Server/Coderr.Server.Domain/Modules/Similarities/SimilarityValue.cs
@@ -1,82 +1,82 @@
-using System;
-using System.Diagnostics;
-
-namespace OneTrueError.App.Modules.Similarities.Domain
-{
-    /// 
-    ///     Holds the similarity value, its percentage and total count of similarities
-    /// 
-    public class SimilarityValue
-    {
-        /// 
-        ///     Initializes a new instance of the  class.
-        /// 
-        /// Normalized value.
-        /// value
-        public SimilarityValue(string value)
-        {
-            if (value == null) throw new ArgumentNullException("value");
-            Value = value;
-        }
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// value
-        /// percentage, 0-100
-        /// number of times we've got this value
-        public SimilarityValue(string value, int percentage, int count)
-        {
-            Value = value;
-            Count = count;
-            Percentage = percentage;
-        }
-
-        /// 
-        ///     Serialization constructor
-        /// 
-        protected SimilarityValue()
-        {
-        }
-
-        /// 
-        ///     Number of times that this item has been added.
-        /// 
-        public int Count { get; private set; }
-
-        /// 
-        ///     Percentage, 1-100.
-        /// 
-        public int Percentage { get; private set; }
-
-        /// 
-        ///     Normalized value
-        /// 
-        public string Value { get; private set; }
-
-        /// 
-        ///     Increase usage of this value
-        /// 
-        /// Total count for all values (not just this one)
-        public void IncreaseUsage(int totalCount)
-        {
-            if (totalCount <= 0) throw new ArgumentOutOfRangeException("totalCount");
-
-            Count += 1;
-            Percentage = Count*100/totalCount;
-            if (Percentage > 100)
-                Debugger.Break();
-        }
-
-        /// 
-        ///     Each time a new value is added for a similarity we have to recalculate the others.
-        /// 
-        /// 
-        public void Recalculate(int totalCount)
-        {
-            if (totalCount <= 0) throw new ArgumentOutOfRangeException("totalCount");
-
-            Percentage = Count*100/totalCount;
-        }
-    }
+using System;
+using System.Diagnostics;
+
+namespace Coderr.Server.Domain.Modules.Similarities
+{
+    /// 
+    ///     Holds the similarity value, its percentage and total count of similarities
+    /// 
+    public class SimilarityValue
+    {
+        /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        /// Normalized value.
+        /// value
+        public SimilarityValue(string value)
+        {
+            if (value == null) throw new ArgumentNullException("value");
+            Value = value;
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// value
+        /// percentage, 0-100
+        /// number of times we've got this value
+        public SimilarityValue(string value, int percentage, int count)
+        {
+            Value = value;
+            Count = count;
+            Percentage = percentage;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected SimilarityValue()
+        {
+        }
+
+        /// 
+        ///     Number of times that this item has been added.
+        /// 
+        public int Count { get; private set; }
+
+        /// 
+        ///     Percentage, 1-100.
+        /// 
+        public int Percentage { get; private set; }
+
+        /// 
+        ///     Normalized value
+        /// 
+        public string Value { get; private set; }
+
+        /// 
+        ///     Increase usage of this value
+        /// 
+        /// Total count for all values (not just this one)
+        public void IncreaseUsage(int totalCount)
+        {
+            if (totalCount <= 0) throw new ArgumentOutOfRangeException("totalCount");
+
+            Count += 1;
+            Percentage = Count*100/totalCount;
+            if (Percentage > 100)
+                Debugger.Break();
+        }
+
+        /// 
+        ///     Each time a new value is added for a similarity we have to recalculate the others.
+        /// 
+        /// 
+        public void Recalculate(int totalCount)
+        {
+            if (totalCount <= 0) throw new ArgumentOutOfRangeException("totalCount");
+
+            Percentage = Count*100/totalCount;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/Tags/ITagsRepository.cs b/src/Server/Coderr.Server.Domain/Modules/Tags/ITagsRepository.cs
new file mode 100644
index 00000000..ef656db1
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/Tags/ITagsRepository.cs
@@ -0,0 +1,51 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Modules.Tags
+{
+    /// 
+    ///     Repository for tags
+    /// 
+    public interface ITagsRepository
+    {
+        /// 
+        ///     Add a new tag
+        /// 
+        /// incident that the tag is for
+        /// tag collection
+        /// task
+        Task AddAsync(int incidentId, Tag[] tags);
+
+        Task UpdateTags(int incidentId, string[] tagsToAdd, string[] tagsToRemove);
+
+        Task AddTag(int incidentId, string tag);
+
+        /// 
+        ///     Get a list of tags for an application
+        /// 
+        /// application to get tags for
+        /// List of tags (or an empty list)
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        Task> GetApplicationTagsAsync(int applicationId);
+
+        /// 
+        ///     Get a list of tags for a specific incident
+        /// 
+        /// incident to get tags for
+        /// List of tags (or an empty list)
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        Task> GetIncidentTagsAsync(int incidentId);
+
+        /// 
+        ///     Get a list of tags for an application
+        /// 
+        /// application to get tags for
+        /// incident to get tags for
+        /// List of tags (or an empty list)
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        Task> GetTagsAsync(int? applicationId, int? incidentId);
+
+        Task> GetNewIncidentsForTag(int? applicationId, string tag);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Domain/Tag.cs b/src/Server/Coderr.Server.Domain/Modules/Tags/Tag.cs
similarity index 89%
rename from src/Server/OneTrueError.App/Modules/Tagging/Domain/Tag.cs
rename to src/Server/Coderr.Server.Domain/Modules/Tags/Tag.cs
index fe7dc507..be60eca1 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Domain/Tag.cs
+++ b/src/Server/Coderr.Server.Domain/Modules/Tags/Tag.cs
@@ -1,45 +1,50 @@
-using System;
-
-namespace OneTrueError.App.Modules.Tagging.Domain
-{
-    /// 
-    ///     Stack overflow tag
-    /// 
-    public class Tag
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// name
-        /// order. 1 = first.
-        /// name
-        public Tag(string name, int orderNumber)
-        {
-            if (name == null) throw new ArgumentNullException("name");
-            Name = name;
-            OrderNumber = orderNumber;
-        }
-
-        /// 
-        ///     Serialization constructor
-        /// 
-        protected Tag()
-        {
-        }
-
-        /// 
-        ///     Identity
-        /// 
-        public int Id { get; set; }
-
-        /// 
-        ///     Name
-        /// 
-        public string Name { get; set; }
-
-        /// 
-        ///     Order
-        /// 
-        public int OrderNumber { get; set; }
-    }
+using System;
+
+namespace Coderr.Server.Domain.Modules.Tags
+{
+    /// 
+    ///     Stack overflow tag
+    /// 
+    public class Tag
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// name
+        /// order. 1 = first.
+        /// name
+        public Tag(string name, int orderNumber)
+        {
+            if (name == null) throw new ArgumentNullException("name");
+            Name = name;
+            OrderNumber = orderNumber;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected Tag()
+        {
+        }
+
+        /// 
+        ///     Identity
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     Name
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Order
+        /// 
+        public int OrderNumber { get; set; }
+
+        public override string ToString()
+        {
+            return Name;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/UserNotifications/BrowserSubscription.cs b/src/Server/Coderr.Server.Domain/Modules/UserNotifications/BrowserSubscription.cs
new file mode 100644
index 00000000..edc4ff62
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/UserNotifications/BrowserSubscription.cs
@@ -0,0 +1,26 @@
+using System;
+
+namespace Coderr.Server.Domain.Modules.UserNotifications
+{
+    public class BrowserSubscription
+    {
+        public int AccountId { get; set; }
+
+        public DateTime CreatedAtUtc { get; set; } = DateTime.UtcNow;
+
+        /// 
+        /// 
+        public string AuthenticationSecret { get; set; }
+
+        public string Endpoint { get; set; }
+
+        public DateTime? ExpiresAtUtc { get; set; }
+
+        public int Id { get; set; }
+
+        /// 
+        ///     https://developer.mozilla.org/en-US/docs/Web/API/PushSubscription/getKey
+        /// 
+        public string PublicKey { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/Modules/UserNotifications/IUserNotificationsRepository.cs b/src/Server/Coderr.Server.Domain/Modules/UserNotifications/IUserNotificationsRepository.cs
new file mode 100644
index 00000000..07396778
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/UserNotifications/IUserNotificationsRepository.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Domain.Modules.UserNotifications
+{
+    public interface IUserNotificationsRepository
+    {
+        /// 
+        ///     Get application settings for all users.
+        /// 
+        /// applicationId
+        /// Default setting will be returned for users that do not have any application specific.
+        /// applicationId
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
+        Task> GetAllAsync(int applicationId);
+
+        Task> GetSubscriptions(int accountId);
+        Task Delete(BrowserSubscription subscription);
+
+    }
+}
diff --git a/src/Server/Coderr.Server.Domain/Modules/UserNotifications/NotificationState.cs b/src/Server/Coderr.Server.Domain/Modules/UserNotifications/NotificationState.cs
new file mode 100644
index 00000000..f1f7d431
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/Modules/UserNotifications/NotificationState.cs
@@ -0,0 +1,38 @@
+namespace Coderr.Server.Domain.Modules.UserNotifications
+{
+    /// 
+    ///     Type of notification to use
+    /// 
+    public enum NotificationState
+    {
+        /// 
+        ///     Use global setting
+        /// 
+        UseGlobalSetting = 1,
+
+        /// 
+        ///     Do not notify
+        /// 
+        Disabled = 2,
+
+        /// 
+        ///     By cellphone (text message)
+        /// 
+        Cellphone = 3,
+
+        /// 
+        ///     By email
+        /// 
+        Email = 4,
+
+        /// 
+        ///     Send a browser notification to the user
+        /// 
+        /// 
+        ///     
+        ///         Requires that the user have approved it (by a javascript request).
+        ///     
+        /// 
+        BrowserNotification = 5
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Core/Notifications/UserNotificationSettings.cs b/src/Server/Coderr.Server.Domain/Modules/UserNotifications/UserNotificationSettings.cs
similarity index 76%
rename from src/Server/OneTrueError.App/Core/Notifications/UserNotificationSettings.cs
rename to src/Server/Coderr.Server.Domain/Modules/UserNotifications/UserNotificationSettings.cs
index 4a6f9b14..88a8e6cb 100644
--- a/src/Server/OneTrueError.App/Core/Notifications/UserNotificationSettings.cs
+++ b/src/Server/Coderr.Server.Domain/Modules/UserNotifications/UserNotificationSettings.cs
@@ -1,70 +1,82 @@
-using System;
-
-namespace OneTrueError.App.Core.Notifications
-{
-    /// 
-    ///     Account settings for notifications
-    /// 
-    public class UserNotificationSettings
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// Account id
-        /// Application (0 = general setting)
-        /// accountId
-        public UserNotificationSettings(int accountId, int applicationId)
-        {
-            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
-            AccountId = accountId;
-            ApplicationId = applicationId;
-        }
-
-        /// 
-        ///     Serialization constructor
-        /// 
-        protected UserNotificationSettings()
-        {
-        }
-
-        /// 
-        ///     Account id
-        /// 
-        public int AccountId { get; set; }
-
-        /// 
-        ///     Application id (0 = general configuration)
-        /// 
-        public int ApplicationId { get; set; }
-
-        /// 
-        ///     Notify when a report spike is detected for an application
-        /// 
-        public NotificationState ApplicationSpike { get; set; }
-
-        /// 
-        ///     How to notify when a new incident is created (received a new unique exception).
-        /// 
-        public NotificationState NewIncident { get; set; }
-
-        /// 
-        ///     Notify each time a new exception is received (no matter if it's unique or not)
-        /// 
-        public NotificationState NewReport { get; set; }
-
-        /// 
-        ///     Notify when we received a report for an incident that has been closed
-        /// 
-        public NotificationState ReopenedIncident { get; set; }
-
-        /// 
-        ///     Notify when a user have written an error description
-        /// 
-        public NotificationState UserFeedback { get; set; }
-
-        /// 
-        ///     Send a weekly summary for all applications that the user is a member of.
-        /// 
-        public NotificationState WeeklySummary { get; set; }
-    }
+using System;
+
+namespace Coderr.Server.Domain.Modules.UserNotifications
+{
+    /// 
+    ///     Account settings for notifications
+    /// 
+    public class UserNotificationSettings
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Account id
+        /// Application (0 = general setting)
+        /// accountId
+        public UserNotificationSettings(int accountId, int applicationId)
+        {
+            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
+            AccountId = accountId;
+            ApplicationId = applicationId;
+            if (applicationId != 0)
+                return;
+
+            ApplicationSpike = NotificationState.Disabled;
+            NewIncident = NotificationState.Disabled;
+            ReopenedIncident = NotificationState.Disabled;
+            UserFeedback = NotificationState.Disabled;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected UserNotificationSettings()
+        {
+        }
+
+        /// 
+        ///     Account id
+        /// 
+        public int AccountId { get; set; }
+
+        /// 
+        ///     Application id (0 = general configuration)
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Notify when a report spike is detected for an application
+        /// 
+        public NotificationState ApplicationSpike { get; set; }
+
+        /// 
+        ///     How to notify when a new incident is created (received a new unique exception).
+        /// 
+        public NotificationState NewIncident { get; set; }
+
+        /// 
+        ///     How to notify when an incident is updated to critical.
+        /// 
+        public NotificationState CriticalIncident { get; set; }
+
+        /// 
+        ///     How to notify when an incident is updated to important.
+        /// 
+        public NotificationState ImportantIncident { get; set; }
+
+        /// 
+        ///     Notify when we received a report for an incident that has been closed
+        /// 
+        public NotificationState ReopenedIncident { get; set; }
+
+        /// 
+        ///     Notify when a user have written an error description
+        /// 
+        public NotificationState UserFeedback { get; set; }
+
+        /// 
+        ///     Send a weekly summary for all applications that the user is a member of.
+        /// 
+        public NotificationState WeeklySummary { get; set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Domain/ReadMe.md b/src/Server/Coderr.Server.Domain/ReadMe.md
new file mode 100644
index 00000000..facfe97c
--- /dev/null
+++ b/src/Server/Coderr.Server.Domain/ReadMe.md
@@ -0,0 +1,5 @@
+Coderr domain entities
+======================
+
+Contains all domain entities in complete isolation, focusing on defining the entities and their abilities.
+Should not depend on anything else in the system.
diff --git a/src/Server/Coderr.Server.Infrastructure.Tests/Coderr.Server.Infrastructure.Tests.csproj b/src/Server/Coderr.Server.Infrastructure.Tests/Coderr.Server.Infrastructure.Tests.csproj
new file mode 100644
index 00000000..14620995
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure.Tests/Coderr.Server.Infrastructure.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+  
+    netcoreapp3.1
+    false
+  
+
+  
+    
+    
+    
+    
+      all
+      runtime; build; native; contentfiles; analyzers; buildtransitive
+    
+    
+    
+    
+  
+
+  
+    
+  
+
+
diff --git a/src/Server/Coderr.Server.Infrastructure.Tests/Messaging/Disk/Queue/DiskFileTests.cs b/src/Server/Coderr.Server.Infrastructure.Tests/Messaging/Disk/Queue/DiskFileTests.cs
new file mode 100644
index 00000000..fa551371
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure.Tests/Messaging/Disk/Queue/DiskFileTests.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.Infrastructure.Messaging.Disk;
+using Coderr.Server.Infrastructure.Messaging.Disk.Queue;
+using FluentAssertions;
+using Xunit;
+
+namespace Coderr.Server.Infrastructure.Tests.Messaging.Disk.Queue
+{
+    public class DiskFileTests : IDisposable
+    {
+        private readonly DiskFile _sut;
+        private readonly string _fullPath;
+
+        public DiskFileTests()
+        {
+            var directory = Path.GetTempPath();
+            var file = Path.GetTempFileName();
+            _fullPath = Path.Combine(directory, file);
+            _sut = new DiskFile("MyQueeue", _fullPath);
+
+        }
+
+        [Fact]
+        public async Task Should_be_able_to_wait_on_record_directly()
+        {
+            await _sut.OpenAsync();
+            var t2 = _sut.DequeueAsync(TimeSpan.FromSeconds(1)).ContinueWith(MyAction);
+
+            await _sut.EnqueueAsync("HEllo wold");
+
+            await t2;
+        }
+
+        [Fact]
+        public async Task Should_ignore_invalid_record_in_the_middle()
+        {
+            await _sut.OpenAsync();
+            await _sut.EnqueueAsync("Hello world");
+            await _sut.CloseReadAsync();
+            using (var stream = File.OpenWrite(_fullPath))
+            {
+                stream.Seek(0, SeekOrigin.End);
+                var buf = new byte[20];
+                await stream.WriteAsync(buf, 0, buf.Length);
+            }
+            await _sut.OpenAsync();
+            await _sut.EnqueueAsync("Hello world2");
+            var record1 = await _sut.DequeueAsync(TimeSpan.Zero);
+            var record2 = await _sut.DequeueAsync(TimeSpan.Zero);
+
+
+
+            _sut.NumberOfAvailableRecords.Should().Be(0);
+            record1.Entity.Should().Be("Hello world");
+            record2.Entity.Should().Be("Hello world2");
+        }
+
+        private void MyAction(Task> obj)
+        {
+            
+        }
+
+        public void Dispose()
+        {
+            _sut?.Dispose();
+            File.Delete(_fullPath);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Infrastructure.Tests/UnitTest1.cs b/src/Server/Coderr.Server.Infrastructure.Tests/UnitTest1.cs
new file mode 100644
index 00000000..902a36e3
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure.Tests/UnitTest1.cs
@@ -0,0 +1,14 @@
+using System;
+using Xunit;
+
+namespace Coderr.Server.Infrastructure.Tests
+{
+    public class UnitTest1
+    {
+        [Fact]
+        public void Test1()
+        {
+
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Infrastructure/ApplicationVersionComparer.cs b/src/Server/Coderr.Server.Infrastructure/ApplicationVersionComparer.cs
new file mode 100644
index 00000000..c6f1a1c3
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/ApplicationVersionComparer.cs
@@ -0,0 +1,46 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coderr.Server.Infrastructure
+{
+    public class ApplicationVersionComparer : IComparer
+    {
+        public int Compare(string first, string second)
+        {
+            if (string.IsNullOrWhiteSpace(first) || string.IsNullOrWhiteSpace(second))
+                return 0;
+
+            int value;
+
+            var pos = first.IndexOf('-');
+            if (pos != -1)
+                first = first.Substring(0, pos);
+            var firstVersion = first.Split('.').Select(y => int.TryParse(y, out value) ? value : -1).ToArray();
+            if (firstVersion.Any(x => x == -1))
+                return 0;
+
+            pos = second.IndexOf('-');
+            if (pos != -1)
+                second = second.Substring(0, pos);
+            var secondVersion = second.Split('.').Select(y => int.TryParse(y, out value) ? value : -1).ToArray();
+            if (secondVersion.Any(x => x == -1))
+                return 0;
+
+            for (var i = 0; i < firstVersion.Length; i++)
+            {
+                if (secondVersion.Length <= i)
+                    return 1;
+
+                if (firstVersion[i] < secondVersion[i])
+                    return -1;
+                if (firstVersion[i] > secondVersion[i])
+                    return 1;
+            }
+
+            if (firstVersion.Length < secondVersion.Length)
+                return -1;
+
+            return 0;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj b/src/Server/Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj
new file mode 100644
index 00000000..ac4d1138
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj
@@ -0,0 +1,30 @@
+
+  
+    netstandard2.0
+    Coderr.Server.Infrastructure
+    Coderr.Server.Infrastructure
+    Debug;Release;Premise
+  
+  
+    
+    
+    
+    
+    
+    
+    
+  
+  
+    
+    
+  
+  
+    
+    
+  
+  
+    
+      3.1.22
+    
+  
+
diff --git a/src/Server/Coderr.Server.Infrastructure/CoderrDtoSerializer.cs b/src/Server/Coderr.Server.Infrastructure/CoderrDtoSerializer.cs
new file mode 100644
index 00000000..0e3dbd1b
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/CoderrDtoSerializer.cs
@@ -0,0 +1,50 @@
+using System;
+using Coderr.Server.Infrastructure.Messaging;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.Infrastructure
+{
+    /// 
+    ///     Internal serializer, used only to store stuff that aren't exposed outside the App/data namespace.
+    /// 
+    public static class CoderrDtoSerializer
+    {
+        private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
+        {
+            ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
+            ContractResolver = new IncludeNonPublicMembersContractResolver()
+        };
+
+        /// 
+        ///     Intern
+        /// 
+        /// 
+        /// 
+        /// 
+        public static T Deserialize(string json)
+        {
+            return JsonConvert.DeserializeObject(json, Settings);
+        }
+
+        /// 
+        ///     Deserialize JSON
+        /// 
+        /// JSON
+        /// type being deserialized
+        /// object
+        public static object Deserialize(string json, Type type)
+        {
+            return JsonConvert.DeserializeObject(json, type, Settings);
+        }
+
+        /// 
+        ///     Serialize to JSON
+        /// 
+        /// entity
+        /// JSON
+        public static string Serialize(object data)
+        {
+            return JsonConvert.SerializeObject(data);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Configuration/BaseConfiguration.cs b/src/Server/Coderr.Server.Infrastructure/Configuration/BaseConfiguration.cs
new file mode 100644
index 00000000..15abd25f
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Configuration/BaseConfiguration.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using Coderr.Server.Abstractions.Config;
+
+namespace Coderr.Server.Infrastructure.Configuration
+{
+    /// 
+    ///     Base configuration for the Coderr service.
+    /// 
+    public sealed class BaseConfiguration : IConfigurationSection
+    {
+        /// 
+        ///     allow new users to register accounts.
+        /// 
+        /// 
+        ///     
+        ///         null = not configured = allow.
+        ///     
+        /// 
+        public bool? AllowRegistrations { get; set; }
+
+        /// 
+        ///     Base URL for the home page, including protocol (http:// or https://)
+        /// 
+        public Uri BaseUrl { get; set; }
+
+        /// 
+        ///     Address used as "From" in all emails sent by the system.
+        /// 
+        public string SenderEmail { get; set; }
+
+        /// 
+        ///     Address to contact when having trouble with Coderr (account issues etc).
+        /// 
+        public string SupportEmail { get; set; }
+
+        string IConfigurationSection.SectionName => "BaseConfig";
+
+        IDictionary IConfigurationSection.ToDictionary()
+        {
+            return this.ToConfigDictionary();
+        }
+
+        void IConfigurationSection.Load(IDictionary settings)
+        {
+            this.AssignProperties(settings);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Configuration/CoderrConfigSection.cs b/src/Server/Coderr.Server.Infrastructure/Configuration/CoderrConfigSection.cs
new file mode 100644
index 00000000..13f73e84
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Configuration/CoderrConfigSection.cs
@@ -0,0 +1,58 @@
+using System.Collections.Generic;
+using Coderr.Server.Abstractions.Config;
+
+namespace Coderr.Server.Infrastructure.Configuration
+{
+    /// 
+    ///     We'll want to track all exceptions for all OTE users so that we can correct bugs in OTE.
+    /// 
+    public sealed class CoderrConfigSection : IConfigurationSection
+    {
+        public CoderrConfigSection()
+        {
+            ActivateTracking = true;
+        }
+
+        /// 
+        ///     Allow us to track exceptions in OTE.
+        /// 
+        public bool ActivateTracking { get; set; }
+
+        /// 
+        ///     Email address that we may contact if we need any further information (will also receive notifications when the
+        ///     errors are corrected).
+        /// 
+        public string ContactEmail { get; set; }
+
+        /// 
+        ///     A fixed identity which identifies this specific installation. You can generate a GUID and then store it.
+        /// 
+        /// 
+        ///     
+        ///         Used to identify the number of installations that have the same issue.
+        ///     
+        /// 
+        public string InstallationId { get; set; }
+
+        /// 
+        /// Estimate of how many unique errors the admin think it has.
+        /// 
+        public int NumberOfUniqueErrors { get; set; }
+
+
+        string IConfigurationSection.SectionName
+        {
+            get { return "ErrorTracking"; }
+        }
+
+        IDictionary IConfigurationSection.ToDictionary()
+        {
+            return this.ToConfigDictionary();
+        }
+
+        void IConfigurationSection.Load(IDictionary settings)
+        {
+            this.AssignProperties(settings);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Configuration/ConfigDictionaryExtensions.cs b/src/Server/Coderr.Server.Infrastructure/Configuration/ConfigDictionaryExtensions.cs
new file mode 100644
index 00000000..cf7befc9
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Configuration/ConfigDictionaryExtensions.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.Infrastructure.Configuration
+{
+    /// 
+    ///     Moves otherwise repeated conversions to a single place.
+    /// 
+    public static class ConfigDictionaryExtensions
+    {
+        /// 
+        ///     Convert dictionary item to a boolean.
+        /// 
+        /// instance
+        /// Key
+        /// Value
+        /// If key is not present. Key name is included in the exception message.
+        /// Value is not a boolean. Includes key name and source value in the exception message.
+        public static bool GetBoolean(this IDictionary dictionary, string name, bool? defaultValue = false)
+        {
+            if (dictionary == null) throw new ArgumentNullException("dictionary");
+            if (name == null) throw new ArgumentNullException("name");
+
+            if (!dictionary.TryGetValue(name, out var value))
+            {
+                if (defaultValue != null)
+                    return defaultValue.Value;
+                throw new ArgumentException($"Failed to find key '{name}' in dictionary.");
+            }
+
+            if (defaultValue != null && value == null)
+                return defaultValue.Value;
+
+            if (!bool.TryParse(value, out var boolValue))
+                throw new FormatException($"Failed to convert '{name}' from value '{value}' to a boolean.");
+
+            return boolValue;
+        }
+
+        /// 
+        ///     Convert dictionary item to an integer.
+        /// 
+        /// instance
+        /// Key
+        /// Value
+        /// If key is not present. Key name is included in the exception message.
+        /// Value is not a boolean. Includes key name and source value in the exception message.
+        [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")]
+        public static int GetInteger(this IDictionary dictionary, string name, int? defaultValue = 0)
+        {
+            if (dictionary == null) throw new ArgumentNullException("dictionary");
+            if (name == null) throw new ArgumentNullException("name");
+
+            if (!dictionary.TryGetValue(name, out var value))
+            {
+                if (defaultValue != null)
+                    return defaultValue.Value;
+                throw new ArgumentException($"Failed to find key '{name}' in dictionary.");
+            }
+
+            if (value == null && defaultValue != null)
+                return defaultValue.Value;
+
+            if (!int.TryParse(value, out var intValue))
+                throw new FormatException($"Failed to convert '{name}' from value '{value}' to an integer.");
+
+            return intValue;
+        }
+
+        /// 
+        ///     Get a string value.
+        /// 
+        /// instance
+        /// Key
+        /// Value
+        /// If key is not present. Key name is included in the exception message.
+        public static string GetString(this IDictionary dictionary, string name, bool requireParameter = true)
+        {
+            if (dictionary == null) throw new ArgumentNullException("dictionary");
+            if (name == null) throw new ArgumentNullException("name");
+
+            if (dictionary.TryGetValue(name, out var value))
+                return value;
+
+            if (requireParameter)
+                throw new ArgumentException($"Failed to find key '{name}' in dictionary.");
+
+            return null;
+        }
+
+        /// 
+        ///     Get a string value.
+        /// 
+        /// instance
+        /// Key
+        /// Value to return if given key is not found.
+        /// Value if key is found; otherwise given default value.
+        /// If key is not present. Key name is included in the exception message.
+        public static string GetString(this IDictionary dictionary, string name, string defaultValue)
+        {
+            if (dictionary == null) throw new ArgumentNullException("dictionary");
+            if (name == null) throw new ArgumentNullException("name");
+            return !dictionary.TryGetValue(name, out var value)
+                ? defaultValue
+                : value;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Configuration/Database/DatabaseStore.cs b/src/Server/Coderr.Server.Infrastructure/Configuration/Database/DatabaseStore.cs
new file mode 100644
index 00000000..3e870864
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Configuration/Database/DatabaseStore.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using Coderr.Server.Abstractions.Config;
+using Griffin.Data;
+
+namespace Coderr.Server.Infrastructure.Configuration.Database
+{
+    /// 
+    ///     Uses a DB to store configuration.
+    /// 
+    /// 
+    ///     
+    ///         Items are cached for 30 seconds to avoid loading the DB.
+    ///     
+    /// 
+    public class DatabaseStore : ConfigurationStore
+    {
+        private static readonly Dictionary _cachedItems = new Dictionary();
+        private readonly Func _connectionFactory;
+
+        public DatabaseStore(Func connectionFactory)
+        {
+            _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
+        }
+
+        protected DatabaseStore()
+        {
+
+        }
+
+        /// 
+        ///     Load a settings section
+        /// 
+        /// Type of section
+        /// Category if found; otherwise null.
+        public override T Load()
+        {
+            if (TryGetCachedItem(out T tValue))
+                return tValue;
+
+            var section = new T();
+            using (var connection = OpenConnectionFor())
+            {
+                using (var cmd = connection.CreateCommand())
+                {
+                    cmd.CommandText = "SELECT Name, Value FROM Settings WHERE section = @section";
+                    cmd.AddParameter("section", section.SectionName);
+                    using (var reader = cmd.ExecuteReader())
+                    {
+                        var items = new Dictionary();
+                        while (reader.Read())
+                        {
+                            var name = reader.GetString(0);
+                            var dbValue = reader.GetValue(1);
+                            var value = dbValue is DBNull ? null : (string) dbValue;
+                            items[name] = value;
+                        }
+
+                        // all configuration classes should have defaults.
+                        if (items.Count == 0)
+                            return new T(); 
+
+                        section.Load(items);
+                    }
+                }
+            }
+
+            SetCache(section);
+            return section;
+        }
+
+        public override void Store(IConfigurationSection section)
+        {
+            SetCache(section);
+
+
+            using (var connection = OpenConnectionFor(section.GetType()))
+            {
+                using (var cmd = connection.CreateCommand())
+                {
+                    cmd.CommandText = "DELETE FROM Settings WHERE section = @section";
+                    cmd.AddParameter("section", section.SectionName);
+                    cmd.ExecuteNonQuery();
+                }
+                var items = section.ToDictionary();
+                if (items.Count == 0)
+                {
+                    return;
+                }
+                    
+                using (var cmd = connection.CreateCommand())
+                {
+                    var index = 0;
+                    foreach (var kvp in items)
+                    {
+                        cmd.CommandText +=
+                            string.Format(
+                                "INSERT INTO Settings (Section, Name, Value) VALUES(@section, @name{0}, @value{0})",
+                                index);
+                        cmd.AddParameter("name" + index, kvp.Key);
+                        cmd.AddParameter("value" + index, kvp.Value);
+                        ++index;
+                    }
+
+                    cmd.AddParameter("section", section.SectionName);
+                    cmd.ExecuteNonQuery();
+                }
+            }
+        }
+
+        /// 
+        ///     Allow connections to be created by another peep.
+        /// 
+        /// 
+        /// open connection
+        protected virtual IDbConnection OpenConnectionFor()
+        {
+            return OpenConnectionFor(typeof(T));
+        }
+
+        /// 
+        ///     Allow connections to be created by another peep.
+        /// 
+        /// A IConfigurationSection type
+        /// open connection
+        protected virtual IDbConnection OpenConnectionFor(Type configClassType)
+        {
+            if (configClassType == null) throw new ArgumentNullException(nameof(configClassType));
+            return _connectionFactory();
+        }
+
+        protected virtual void SetCache(IConfigurationSection section)
+        {
+            lock (_cachedItems)
+            {
+                _cachedItems[section.GetType()] = new Wrapper {AddedAtUtc = DateTime.UtcNow, Value = section};
+            }
+        }
+
+        protected virtual bool TryGetCachedItem(out T tValue)
+        {
+            lock (_cachedItems)
+            {
+                if (_cachedItems.TryGetValue(typeof(T), out var t) && !t.HasExpired())
+                {
+                    tValue = (T) t.Value;
+                    return true;
+                }
+            }
+
+            tValue = default(T);
+            return false;
+        }
+
+        private class Wrapper
+        {
+            public DateTime AddedAtUtc { get; set; }
+            public object Value { get; set; }
+
+            public bool HasExpired()
+            {
+                return DateTime.UtcNow.Subtract(AddedAtUtc).TotalSeconds >= 60;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/IDatabaseUtilities.cs b/src/Server/Coderr.Server.Infrastructure/IDatabaseUtilities.cs
new file mode 100644
index 00000000..600f7991
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/IDatabaseUtilities.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Data;
+
+namespace Coderr.Server.Infrastructure
+{
+    public interface ISetupDatabaseTools
+    {
+        /// 
+        ///     Create all tables in the new DB.
+        /// 
+        void CreateTables();
+
+        /// 
+        ///     Checks if the tables exists and are for the current DB schema.
+        /// 
+        bool IsTablesInstalled();
+
+        /// 
+        ///     Open a new connection.
+        /// 
+        /// Connection
+        IDbConnection OpenConnection();
+
+        void TestConnection(string connectionString);
+        bool IsConfigurationComplete(string connectionString);
+        void MarkConfigurationAsComplete();
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/IPrincipalAccessor.cs b/src/Server/Coderr.Server.Infrastructure/IPrincipalAccessor.cs
new file mode 100644
index 00000000..af0443a0
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/IPrincipalAccessor.cs
@@ -0,0 +1,21 @@
+//using System.Security.Claims;
+
+//namespace Coderr.Server.Infrastructure
+//{
+//    public class PrincipalWrapper
+//    {
+//        private ClaimsPrincipal _principal;
+
+//        public ClaimsPrincipal Principal
+//        {
+//            get { return _principal; }
+//            set
+//            {
+//                if (value == null || !value.Identity.IsAuthenticated)
+//                    return;
+
+//                _principal = value;
+//            }
+//        }
+//    }
+//}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/IUrlHelper.cs b/src/Server/Coderr.Server.Infrastructure/IUrlHelper.cs
new file mode 100644
index 00000000..0c46c409
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/IUrlHelper.cs
@@ -0,0 +1,7 @@
+namespace Coderr.Server.Infrastructure
+{
+    public interface IUrlHelper
+    {
+        string ToAbsolutePath(string virtualPath);
+    }
+}
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/ClaimDto.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/ClaimDto.cs
new file mode 100644
index 00000000..3310d235
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/ClaimDto.cs
@@ -0,0 +1,32 @@
+using System.Security.Claims;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk
+{
+    /// 
+    ///     To serialize claims
+    /// 
+    public class ClaimDto
+    {
+        public ClaimDto(Claim claim)
+        {
+            Value = claim.Value;
+            ValueType = claim.ValueType;
+            ClaimType = claim.Type;
+        }
+
+        protected ClaimDto()
+        {
+        }
+
+        public string ClaimType { get; set; }
+
+
+        public string Value { get; set; }
+        public string ValueType { get; set; }
+
+        public Claim ToClaim()
+        {
+            return new Claim(ClaimType, Value, ValueType);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DequeueTracking.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DequeueTracking.cs
new file mode 100644
index 00000000..68beb0de
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DequeueTracking.cs
@@ -0,0 +1,134 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using log4net;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk
+{
+    /// 
+    ///     Keeps track of which entries we have dequeued in the first queue file.
+    /// 
+    public class DequeueTracking : IDisposable
+    {
+        private readonly byte[] _buffer = new byte[65535];
+        private readonly string _fullPath;
+        private readonly string _queueName;
+        private FileStream _fileStream;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(DequeueTracking));
+
+        public DequeueTracking(string queueDirectory, string queueName)
+        {
+            if (queueDirectory == null) throw new ArgumentNullException(nameof(queueDirectory));
+            _queueName = queueName ?? throw new ArgumentNullException(nameof(queueName));
+            _fullPath = Path.Combine(queueDirectory, queueName + ".meta");
+        }
+
+        /// 
+        ///     Last record that we dequeued (i.e. we should be dequeuing the entry after the specified one).
+        /// 
+        public int LastReadRecord { get; private set; } = -1;
+
+        public void Dispose()
+        {
+            _fileStream?.Dispose();
+        }
+
+        public async Task CloseAsync()
+        {
+            await _fileStream.FlushAsync();
+            _fileStream.Close();
+        }
+
+        public async Task FlushAsync()
+        {
+            await _fileStream.FlushAsync();
+        }
+
+        public async Task OpenAsync(TimeSpan timeout)
+        {
+            _logger.Debug($"[{_queueName}] Opening meta file...");
+
+            // We need this loop for web applications
+            // where the previous instance haven't shutdown completely before the new one
+            // is started.
+            var attemptsLeft = timeout.TotalSeconds;
+            while (attemptsLeft-- > 0)
+            {
+                try
+                {
+                    _fileStream = new FileStream(_fullPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read,
+                        8192,
+                        FileOptions.SequentialScan | FileOptions.Asynchronous);
+                    _logger.Debug($"[{_queueName}] Opened file successfully...");
+                    break;
+                }
+                catch (Exception ex)
+                {
+                    _logger.Debug($"[{_queueName}] Failed, will try again in 1 second ({ex.Message})...");
+                    await Task.Delay(1000);
+                    if (attemptsLeft == 0)
+                        //_logger.Fatal($"[{_queueName}] Failed to open metafile ({ex.Message})...");
+                        throw new InvalidOperationException("Failed to open meta file 10 times.", ex);
+                }
+            }
+
+            _logger.Debug($"[{_queueName}] Reading records...");
+            while (true)
+            {
+                var typeOfRecord = _fileStream.ReadByte();
+                if (typeOfRecord == -1)
+                    break;
+
+                if (typeOfRecord == 1)
+                {
+                    var readBytes = await _fileStream.ReadAsync(_buffer, 0, 4);
+                    if (readBytes == 0)
+                        break;
+
+                    if (readBytes != 4)
+                        throw new InvalidOperationException("Corrupt meta file, uh oh.");
+
+                    LastReadRecord = BitConverter.ToInt32(_buffer, 0);
+                }
+            }
+
+            _logger.Debug($"[{_queueName}] last read record: " + LastReadRecord);
+        }
+
+        /// 
+        ///     Reset file, since we are starting to read from a new queue file.
+        /// 
+        /// 
+        public async Task ResetAsync()
+        {
+            _fileStream.SetLength(0);
+            LastReadRecord = -1;
+            await _fileStream.FlushAsync();
+        }
+
+        /// 
+        ///     Store where the next message to read is located.
+        /// 
+        /// Should be the position to the record separator and not the start of the message.
+        /// 
+        public async Task WriteReadRecord(int filePosition)
+        {
+            //if (_fileName != fileName)
+            //{
+            //    _buffer[0] = (byte)Encoding.UTF8.GetBytes(fileName, 0, fileName.Length, _buffer, 1);
+            //    _fileName = fileName;
+            //    var count = 1 + _buffer[0];
+            //    await _fileStream.WriteAsync(_buffer, 0, count);
+            //}
+            _fileStream.WriteByte(1);
+            var buf = BitConverter.GetBytes(filePosition);
+            await _fileStream.WriteAsync(buf, 0, buf.Length);
+        }
+    }
+
+    public enum MetaFileRecordType
+    {
+        OpenFile,
+        Dequeue
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DiskEntry.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DiskEntry.cs
new file mode 100644
index 00000000..c6c8b006
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DiskEntry.cs
@@ -0,0 +1,13 @@
+using DotNetCqs;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk
+{
+    public class DiskEntry
+    {
+        public string AuthenticationType { get; set; }
+        public Message Body { get; set; }
+
+        public ClaimDto[] Claims { get; set; }
+        public string TypeName { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DiskQueueAdapter.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DiskQueueAdapter.cs
new file mode 100644
index 00000000..711ec013
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DiskQueueAdapter.cs
@@ -0,0 +1,25 @@
+using Coderr.Server.Infrastructure.Messaging.Disk.DotNetCqs;
+using Coderr.Server.Infrastructure.Messaging.Disk.Queue;
+using DotNetCqs;
+using DotNetCqs.Queues;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk
+{
+    public class DiskQueueAdapter : IMessageQueue
+    {
+        private readonly DiskQueue _queue;
+
+        public DiskQueueAdapter(string name, DiskQueue queue)
+        {
+            _queue = queue;
+            Name = name;
+        }
+
+        public IMessageQueueSession BeginSession()
+        {
+            return new DiskQueueSession(_queue);
+        }
+
+        public string Name { get; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DotNetCqs/DiskQueueProvider.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DotNetCqs/DiskQueueProvider.cs
new file mode 100644
index 00000000..df640e50
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DotNetCqs/DiskQueueProvider.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Infrastructure.Messaging.Disk.Queue;
+using DotNetCqs;
+using DotNetCqs.Queues;
+using log4net;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.DotNetCqs
+{
+    public class DiskQueueProvider : IMessageQueueProvider, IDisposable
+    {
+        private readonly string _queueDirectory;
+
+        private readonly Dictionary> _queues =
+            new Dictionary>();
+
+        private QueueLockFile _lockFile;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(DiskQueueProvider));
+
+        public DiskQueueProvider(string queueDirectory)
+        {
+            _queueDirectory = queueDirectory;
+        }
+
+        public Func> ShutdownRequested { get; set; }
+
+        public void Dispose()
+        {
+            if (_queues.Any()) Shutdown();
+        }
+
+        public IMessageQueue Open(string queueName)
+        {
+            
+            if (_queues.TryGetValue(queueName, out var entry)) return new DiskQueueAdapter(queueName, entry);
+
+            if (_lockFile == null)
+            {
+                _lockFile = new QueueLockFile(_queueDirectory, queueName) {CloseQueueRequested = TriggerQueueEvent};
+                _lockFile.CreateLockFile(TimeSpan.FromSeconds(30)).GetAwaiter().GetResult();
+            }
+
+            var queue = new DiskQueue(_queueDirectory, queueName);
+            _queues.Add(queueName, queue);
+            queue.OpenAsync(TimeSpan.FromSeconds(60)).GetAwaiter().GetResult();
+
+            return new DiskQueueAdapter(queueName, queue);
+        }
+
+        public void Shutdown()
+        {
+            foreach (var queue in _queues)
+            {
+                try
+                {
+                    queue.Value.CloseAsync().GetAwaiter().GetResult();
+                }
+                catch (Exception ex)
+                {
+                    _logger.Error("Failed to close " + queue.Key, ex);
+                }
+            }
+
+            foreach (var queue in _queues) queue.Value.Dispose();
+
+            _queues.Clear();
+            _lockFile.DeleteLockFile();
+        }
+
+        private async Task TriggerQueueEvent()
+        {
+            if (ShutdownRequested == null)
+                return false;
+
+            return await ShutdownRequested();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DotNetCqs/DiskQueueSession.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DotNetCqs/DiskQueueSession.cs
new file mode 100644
index 00000000..f138f8fc
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/DotNetCqs/DiskQueueSession.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Coderr.Server.Infrastructure.Messaging.Disk.Queue;
+using DotNetCqs;
+using DotNetCqs.Queues;
+using log4net;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.DotNetCqs
+{
+    public class DiskQueueSession : IMessageQueueSession
+    {
+        private readonly DiskQueue _queue;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(DiskQueueSession));
+
+        public DiskQueueSession(DiskQueue queue)
+        {
+            _queue = queue;
+        }
+
+        public void Dispose()
+        {
+        }
+
+        public async Task Dequeue(TimeSpan suggestedWaitPeriod)
+        {
+            return await _queue.DequeueAsync();
+        }
+
+        public async Task DequeueWithCredentials(TimeSpan suggestedWaitPeriod)
+        {
+            var msg = await _queue.DequeueAsync();
+            if (msg == null) return null;
+
+            var principal = CreatePrincipal(msg);
+            return new DequeuedMessage(principal, msg);
+        }
+
+        public async Task EnqueueAsync(ClaimsPrincipal principal, IReadOnlyCollection messages)
+        {
+            foreach (var message in messages)
+            {
+                foreach (var claim in principal.Claims) message.Properties[$"X-Claim-{claim.Type}"] = claim.Value;
+
+                message.Properties["X-Principal-AuthenticationType"] = principal.Identity.AuthenticationType;
+                message.Properties["Body-Type"] = message.Body.GetType().FullName;
+                await _queue.EnqueueAsync(message);
+            }
+
+            await _queue.FlushAsync();
+        }
+
+        public async Task EnqueueAsync(IReadOnlyCollection messages)
+        {
+            foreach (var message in messages)
+            {
+                message.Properties["Body-Type"] = message.Body.GetType().FullName;
+                await _queue.EnqueueAsync(message);
+            }
+
+            await _queue.FlushAsync();
+        }
+
+        public async Task EnqueueAsync(ClaimsPrincipal principal, Message message)
+        {
+            foreach (var claim in principal.Claims) message.Properties[$"X-Claim-{claim.Type}"] = claim.Value;
+
+            message.Properties["X-Principal-AuthenticationType"] = principal.Identity.AuthenticationType;
+            message.Properties["Body-Type"] = message.Body.GetType().FullName;
+            await _queue.EnqueueAsync(message);
+            await _queue.FlushAsync();
+        }
+
+        public async Task EnqueueAsync(Message message)
+        {
+            message.Properties["Body-Type"] = message.Body.GetType().FullName;
+            await _queue.EnqueueAsync(message);
+            await _queue.FlushAsync();
+        }
+
+        public Task SaveChanges()
+        {
+            return Task.CompletedTask;
+        }
+
+        private ClaimsPrincipal CreatePrincipal(Message msg)
+        {
+            if (msg.Properties == null)
+            {
+                _logger.Debug("Null principal" + msg.Body);
+                return null;
+            }
+
+            if (!msg.Properties.TryGetValue("X-Principal-AuthenticationType", out var authType))
+                return null;
+
+            var claims = new List();
+            foreach (var property in msg.Properties)
+            {
+                if (!property.Key.StartsWith("X-Claim-"))
+                    continue;
+
+                var name = property.Key.Substring(8);
+                var value = property.Value;
+                claims.Add(new Claim(name, value));
+            }
+
+            return new ClaimsPrincipal(new ClaimsIdentity(claims, authType));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/AdoNetMessageDTO.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/AdoNetMessageDTO.cs
new file mode 100644
index 00000000..863f0b6a
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/AdoNetMessageDTO.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Claims;
+using DotNetCqs;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Dtos
+{
+    public class MessageDto
+    {
+        public MessageDto(ClaimsPrincipal principal, Message message)
+        {
+            if (message == null) throw new ArgumentNullException(nameof(message));
+
+            Properties = message.Properties ?? new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+            if (message.MessageId != Guid.Empty) Properties["X-MessageId"] = message.MessageId.ToString();
+
+            if (message.CorrelationId != Guid.Empty)
+                Properties["X-CorrelationId"] = message.CorrelationId.ToString();
+
+            Body = message.Body;
+            Properties["X-ContentType"] = message.Body.GetType().FullName;
+
+
+            ClaimIdentity = principal == null ? null : new IdentityDto((ClaimsIdentity)principal.Identity);
+        }
+
+        protected MessageDto()
+        {
+        }
+
+        public object Body { get; set; }
+
+        public IdentityDto ClaimIdentity { get; set; }
+
+        public IDictionary Properties { get; set; }
+
+        public void ToMessage(out Message message, out ClaimsPrincipal principal)
+        {
+            var props = new Dictionary(Properties, StringComparer.OrdinalIgnoreCase);
+            var identity = ClaimIdentity?.ToIdentity();
+
+            principal = identity != null ? new ClaimsPrincipal(identity) : null;
+            message = new Message(Body, props) {MessageId = Guid.Parse(props["X-MessageId"])};
+
+            if (props.TryGetValue("X-CorrelationId", out var correlationId))
+            {
+                message.CorrelationId = Guid.Parse(correlationId);
+                props.Remove("X-CorrelationId");
+            }
+
+            props.Remove("X-ContentType");
+            props.Remove("X-MessageId");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/ClaimDto.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/ClaimDto.cs
new file mode 100644
index 00000000..696ce2e3
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/ClaimDto.cs
@@ -0,0 +1,32 @@
+using System.Security.Claims;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Dtos
+{
+    /// 
+    ///     To serialize claims
+    /// 
+    public class ClaimDto
+    {
+        public ClaimDto(Claim claim)
+        {
+            Value = claim.Value;
+            ValueType = claim.ValueType;
+            ClaimType = claim.Type;
+        }
+
+        protected ClaimDto()
+        {
+        }
+
+        public string ClaimType { get; set; }
+
+
+        public string Value { get; set; }
+        public string ValueType { get; set; }
+
+        public Claim ToClaim()
+        {
+            return new Claim(ClaimType, Value, ValueType);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/IdentityDto.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/IdentityDto.cs
new file mode 100644
index 00000000..53489204
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Dtos/IdentityDto.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Dtos
+{
+    /// 
+    ///     Claim serialization
+    /// 
+    public class IdentityDto
+    {
+        public IdentityDto(ClaimsIdentity identity)
+        {
+            AuthenticationType = identity.AuthenticationType;
+            NameClaimType = identity.NameClaimType;
+            RoleClaimType = identity.RoleClaimType;
+            Claims = identity.Claims.Select(x => new ClaimDto(x)).ToList();
+        }
+
+        protected IdentityDto()
+        {
+        }
+
+        public string AuthenticationType { get; set; }
+        public IReadOnlyList Claims { get; set; }
+        public string NameClaimType { get; set; }
+        public string RoleClaimType { get; set; }
+
+        public ClaimsIdentity ToIdentity()
+        {
+            var claims = Claims.Select(x => new Claim(x.ClaimType, x.Value, x.ValueType));
+            return new ClaimsIdentity(claims, AuthenticationType, NameClaimType, RoleClaimType);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/JsonNetDiskSerializer.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/JsonNetDiskSerializer.cs
new file mode 100644
index 00000000..882fa6c3
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/JsonNetDiskSerializer.cs
@@ -0,0 +1,56 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.Infrastructure.Messaging.Disk.Queue;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk
+{
+    public class JsonNetDiskSerializer : IContentSerializer
+    {
+        private static readonly JsonSerializerSettings _settings = new JsonSerializerSettings
+        {
+            ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
+            ContractResolver = new IncludeNonPublicMembersContractResolver(),
+            NullValueHandling = NullValueHandling.Ignore,
+            TypeNameHandling = TypeNameHandling.Auto,
+            TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
+        };
+
+        public Task SerializeAsync(Stream destination, object entity)
+        {
+            var serializer =
+                new JsonSerializer
+                {
+                    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
+                    ContractResolver = new IncludeNonPublicMembersContractResolver(),
+                    NullValueHandling = NullValueHandling.Ignore,
+                    TypeNameHandling = TypeNameHandling.Auto,
+                    TypeNameAssemblyFormatHandling = TypeNameAssemblyFormatHandling.Simple
+                };
+            using (var writer = new StreamWriter(destination, Encoding.UTF8, 65535, true))
+            {
+                serializer.Serialize(writer, entity);
+            }
+
+            return Task.CompletedTask;
+        }
+
+        public Task DeserializeAsync(Stream source, int recordSize, Type entityType)
+        {
+            var serializer =
+                new JsonSerializer
+                {
+                    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
+                    ContractResolver = new IncludeNonPublicMembersContractResolver(),
+                    NullValueHandling = NullValueHandling.Ignore
+                };
+
+            var reader2 = new StreamReader(source, Encoding.UTF8, true, 65535, true);
+            var json = reader2.ReadToEnd();
+            var entry = JsonConvert.DeserializeObject(json, entityType, _settings);
+            return Task.FromResult(entry);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/BufferStream.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/BufferStream.cs
new file mode 100644
index 00000000..b67d7899
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/BufferStream.cs
@@ -0,0 +1,75 @@
+using System;
+using System.IO;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Queue
+{
+    /// 
+    ///     Wraps an internal buffer.
+    /// 
+    public class BufferStream : Stream
+    {
+        private readonly byte[] _buffer;
+        private int _count;
+        private int _offset;
+
+        public BufferStream(byte[] buffer, int offset, int count)
+        {
+            _buffer = buffer;
+            _offset = offset;
+            _count = count;
+        }
+
+        public override bool CanRead { get; } = true;
+        public override bool CanSeek { get; } = false;
+        public override bool CanWrite { get; } = false;
+        public override long Length => _count;
+
+        public override long Position
+        {
+            get => _offset;
+            set
+            {
+                if (value >= _buffer.Length)
+                    throw new ArgumentOutOfRangeException("value", "Larger that the internal buffer.");
+
+                _offset = (int)value;
+            }
+        }
+
+        public override void Flush()
+        {
+        }
+
+        public override int Read(byte[] buffer, int offset, int count)
+        {
+            if (buffer.Length < count)
+                throw new ArgumentOutOfRangeException("count", "Larger than the supplied buffer.");
+            if (offset + count > buffer.Length)
+                throw new ArgumentOutOfRangeException("count", "Offset+Count is larger than the supplied buffer.");
+
+            var toRead = Math.Min(count, _count);
+            if (toRead == 0)
+                return 0;
+
+            Buffer.BlockCopy(_buffer, _offset, buffer, offset, toRead);
+            _offset += toRead;
+            _count -= toRead;
+            return toRead;
+        }
+
+        public override long Seek(long offset, SeekOrigin origin)
+        {
+            throw new NotSupportedException();
+        }
+
+        public override void SetLength(long value)
+        {
+            throw new NotSupportedException();
+        }
+
+        public override void Write(byte[] buffer, int offset, int count)
+        {
+            throw new NotSupportedException();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/DiskFile.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/DiskFile.cs
new file mode 100644
index 00000000..0d773075
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/DiskFile.cs
@@ -0,0 +1,275 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using log4net;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Queue
+{
+    /// 
+    ///     Represents a single file in a queue.
+    /// 
+    /// 
+    ///     
+    ///         The most efficient way to handle files is to use multiple ones. By doing so we can always do sequential reads
+    ///         and writes. Queue can grow without reallocation and we'll delete files once all entries have been read.
+    ///     
+    /// 
+    public class DiskFile : IDisposable
+    {
+        private static readonly byte[] RecordSeparator = {1, 3, 3, 7};
+        private readonly int _bufferSize = 1000000;
+        private readonly SemaphoreSlim _entriesAvailableLock = new SemaphoreSlim(0, int.MaxValue);
+        private readonly string _fullPath;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(DiskFile));
+        private readonly string _queueName;
+        private readonly MemoryStream _serializerStream = new MemoryStream();
+        private readonly SemaphoreSlim _writeLock = new SemaphoreSlim(1, 1);
+        private long _flushedWritePosition;
+        private bool _isShuttingDown;
+        private int _numberOfRecords;
+        private ReadState _readState;
+        private FileStream _readStream;
+        private long _writeFilePosition;
+        private FileStream _writeStream;
+
+        public DiskFile(string queueName, string fullPath)
+        {
+            _queueName = queueName;
+            _fullPath = fullPath ?? throw new ArgumentNullException(nameof(fullPath));
+        }
+
+
+        public DiskFile(string queueName, string fullPath, int bufferSize)
+        {
+            _queueName = queueName;
+            _fullPath = fullPath ?? throw new ArgumentNullException(nameof(fullPath));
+            _bufferSize = bufferSize;
+        }
+
+        /// 
+        ///     Number of bytes allocated by this file.
+        /// 
+        public long CurrentFileSize
+        {
+            get
+            {
+                if (_writeStream != null) return _writeStream.Length;
+                var info = new FileInfo(_fullPath);
+                return info.Length;
+            }
+        }
+
+        /// 
+        ///     Records that have not yet been read.
+        /// 
+        public int NumberOfAvailableRecords => _numberOfRecords;
+
+        /// 
+        ///     Used to serialize queue entries.
+        /// 
+        public IContentSerializer Serializer { get; set; } = new JsonNetDiskSerializer();
+
+
+        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
+        public void Dispose()
+        {
+            _readStream?.Dispose();
+            _writeStream?.Dispose();
+            _entriesAvailableLock.Dispose();
+            _writeLock.Dispose();
+        }
+
+        /// 
+        ///     Close file
+        /// 
+        /// 
+        public Task CloseReadAsync()
+        {
+            _isShuttingDown = true;
+            _entriesAvailableLock.Release();
+
+            if (_readState == null)
+                return Task.CompletedTask;
+
+            _readStream.Close();
+            _readState = null;
+            return Task.CompletedTask;
+        }
+
+        /// 
+        ///     Close file
+        /// 
+        /// 
+        public async Task CloseWriteAsync()
+        {
+            _entriesAvailableLock.Release();
+
+            if (_writeStream == null)
+                return;
+
+            await _writeStream.FlushAsync();
+            _writeStream.Close();
+            _writeStream = null;
+        }
+
+
+        public async Task Delete()
+        {
+            await CloseWriteAsync();
+            await CloseReadAsync();
+            File.Delete(_fullPath);
+        }
+
+        /// 
+        ///     Queue an entry
+        /// 
+        /// entry if found; otherwise null.
+        public async Task> DequeueAsync(TimeSpan timeout)
+        {
+            if (_writeStream != null && !await _entriesAvailableLock.WaitAsync(timeout)) return null;
+
+
+            if (_isShuttingDown) return null;
+
+            if (_writeStream != null && _readState.MustFlushWrite(_flushedWritePosition)) await FlushWriteAsync();
+
+            var record = await _readState.ReadRecord();
+            if (record != null)
+            {
+                Interlocked.Decrement(ref _numberOfRecords);
+                Debug($"[Dequeue] We now have {_numberOfRecords} records left.");
+            }
+
+            return record;
+        }
+
+
+        /// 
+        ///     Enqueue a new item.
+        /// 
+        /// Item to enqueue
+        /// 
+        public async Task EnqueueAsync(TEntity item)
+        {
+            if (_writeStream == null) throw new InvalidOperationException($"File '{_fullPath}' is not open for write.");
+
+            Debug("[Enqueue] acquiring lock");
+            await _writeLock.WaitAsync();
+            int startPos;
+            try
+            {
+                Debug($"[Enqueue] at position {_writeStream.Position}, record number {_numberOfRecords + 1}.");
+                _serializerStream.SetLength(0);
+                await Serializer.SerializeAsync(_serializerStream, item);
+                _serializerStream.Flush();
+                _serializerStream.Position = 0;
+
+                startPos = (int)_writeFilePosition;
+                var buf = BitConverter.GetBytes((int)_serializerStream.Length);
+                await _writeStream.WriteAsync(RecordSeparator, 0, RecordSeparator.Length);
+                await _writeStream.WriteAsync(buf, 0, buf.Length);
+                buf = _serializerStream.GetBuffer();
+                await _writeStream.WriteAsync(buf, 0, (int)_serializerStream.Length);
+                _writeFilePosition += 8 + _serializerStream.Length;
+
+                Interlocked.Increment(ref _numberOfRecords);
+                _entriesAvailableLock.Release(1);
+            }
+            finally
+            {
+                _writeLock.Release();
+            }
+
+            return startPos;
+        }
+
+        public async Task FlushWriteAsync()
+        {
+            await _writeLock.WaitAsync();
+            try
+            {
+                _flushedWritePosition = _writeFilePosition;
+                await _writeStream.FlushAsync();
+            }
+            finally
+            {
+                _writeLock.Release();
+            }
+        }
+
+        /// 
+        ///     Open the file
+        /// 
+        /// 
+        ///     Position to start ready entries from (if we have previously read from this file, we want to
+        ///     resume at the given position).
+        /// 
+        /// We are only writing to the last file, is this it?
+        /// 
+        public async Task OpenAsync(int startOffset = 0, bool openWrite = false)
+        {
+            _isShuttingDown = false;
+
+            // We need this loop for web applications
+            // where the previous instance haven't shutdown completely before the new one
+            // is started.
+            if (openWrite) await OpenWriteStream();
+
+            await OpenReadStream(startOffset);
+        }
+
+        private void Debug(string msg)
+        {
+            if (_queueName != "ErrorReports")
+                return;
+
+            _logger.Debug($"<{_queueName}> {msg}");
+        }
+
+        private async Task OpenReadStream(int startOffset)
+        {
+            _readStream = new FileStream(_fullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, _bufferSize,
+                FileOptions.Asynchronous | FileOptions.SequentialScan);
+
+            if (startOffset > _readStream.Length)
+                throw new InvalidOperationException("Offset cannot be larger than the file.");
+
+            _readState = new ReadState(_readStream, _bufferSize, RecordSeparator, Serializer);
+
+            // Read the last read record.
+            if (startOffset > 0)
+            {
+                _readStream.Position = startOffset;
+                await _readState.AdjustForPositionBug();
+                await DequeueAsync(TimeSpan.FromSeconds(0));
+            }
+
+            Debug("Counting records...");
+            _numberOfRecords = await _readState.CountRecords();
+            Debug($"... {_numberOfRecords} records ..");
+            if (_numberOfRecords > 0) _entriesAvailableLock.Release(_numberOfRecords);
+        }
+
+        private async Task OpenWriteStream()
+        {
+            var attemptsLeft = 10;
+            while (attemptsLeft-- > 0)
+            {
+                try
+                {
+                    _writeStream = new FileStream(_fullPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite,
+                        _bufferSize,
+                        FileOptions.Asynchronous | FileOptions.SequentialScan);
+                    break;
+                }
+                catch
+                {
+                    await Task.Delay(1000);
+                    if (attemptsLeft == 0)
+                        throw;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/DiskQueue.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/DiskQueue.cs
new file mode 100644
index 00000000..5fb342f0
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/DiskQueue.cs
@@ -0,0 +1,277 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using DotNetCqs;
+using log4net;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Queue
+{
+    public class DiskQueue : IDisposable
+    {
+        private readonly DequeueTracking _dequeueTracking;
+        private readonly LinkedList> _files = new LinkedList>();
+        private readonly string _queueDirectory;
+        private readonly string _queueName;
+        private bool _isShuttingDown;
+        private DateTime _lastQueueFileSizeCheck;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(DiskQueue<>));
+
+        public DiskQueue(string queueDirectory, string queueName)
+        {
+            _queueDirectory = queueDirectory ?? throw new ArgumentNullException(nameof(queueDirectory));
+            _queueName = queueName ?? throw new ArgumentNullException(nameof(queueName));
+
+            // Create a sub directory to make sure that there are no queue collisions
+            _queueDirectory = Path.Combine(_queueDirectory, _queueName);
+
+            _dequeueTracking = new DequeueTracking(queueDirectory, queueName);
+        }
+
+        /// 
+        ///     Flush to disk after each IO operation.
+        /// 
+        /// 
+        ///     
+        ///         Hurts performance but increases reliability. Turn on if you want to make sure that everything exists if the
+        ///         application crashes etc. Turn of if you do batch writes (flush yourself after the batch write).
+        ///     
+        /// 
+        public bool AutoFlush { get; set; }
+
+        /// 
+        ///     Number of items
+        /// 
+        public int Count
+        {
+            get { return _files.Sum(x => x.NumberOfAvailableRecords); }
+        }
+
+        /// 
+        ///     Create a new file once this limit has been reached.
+        /// 
+        /// 
+        ///     Default is 100MB.
+        /// 
+        public int MaximumFileSize { get; set; } = 100000000;
+
+        /// 
+        ///     Reduce number of file size checks to improve IO performance.
+        /// 
+        /// 
+        ///     
+        ///         Checking file size to determine if a new file should be created hurts performance in high IO system. Activating
+        ///         this settings will let queue files grow a bit more before creating a new file, but will improve the overall IO
+        ///         performance.
+        ///     
+        /// 
+        public bool ReduceQueueFileSizeChecks { get; set; }
+
+        /// 
+        ///     Serializer used for the contents that you enqueue.
+        /// 
+        /// 
+        ///     Built in serializer is System.Text.Json
+        /// 
+        public IContentSerializer Serializer { get; set; } = new JsonNetDiskSerializer();
+
+        public void Dispose()
+        {
+            _dequeueTracking.Dispose();
+        }
+
+        /// 
+        ///     Flush everything and close all queue files.
+        /// 
+        /// 
+        public async Task CloseAsync()
+        {
+            _isShuttingDown = true;
+            await FlushAsync();
+            foreach (var file in _files)
+            {
+                await file.CloseWriteAsync();
+                await file.CloseReadAsync();
+            }
+
+            _files.Clear();
+
+            await _dequeueTracking.CloseAsync();
+        }
+
+        /// 
+        ///     Dequeue an entry.
+        /// 
+        /// Dequeued entry if any; otherwise null.
+        public async Task DequeueAsync()
+        {
+            if (_isShuttingDown) return default;
+
+            if (_files.Count == 0)
+                throw new InvalidOperationException("Open queue first.");
+
+            while (true)
+            {
+                var file = _files.First.Value;
+                var record = await file.DequeueAsync(TimeSpan.FromSeconds(10));
+                if (_isShuttingDown)
+                    // since we don't update the position,
+                    // we can safely return null;
+                    return default;
+
+                if (record != null)
+                {
+                    _logger.Debug($"{_queueName} is dequeing {((Message)(object)record.Entity).Body}");
+
+                    // Just read the last record.
+                    if (file.NumberOfAvailableRecords == 0 && _files.Count > 1)
+                    {
+                        await MoveToNextQueueFile();
+                    }
+                    else
+                    {
+                        await _dequeueTracking.WriteReadRecord(record.RecordOffset);
+                        if (AutoFlush)
+                            await _dequeueTracking.FlushAsync();
+                    }
+
+                    return record.Entity;
+                }
+
+                // Only file left means that we are still writing to it.
+                if (_files.Count == 1) return default;
+
+                // We are done with this file.
+                await MoveToNextQueueFile();
+            }
+        }
+
+        /// 
+        ///     Enqueue a new entry.
+        /// 
+        /// 
+        /// 
+        public async Task EnqueueAsync(TEntity entity)
+        {
+            if (entity == null) throw new ArgumentNullException(nameof(entity));
+            if (_files.Count == 0)
+                throw new InvalidOperationException("Open queue first.");
+
+            var file = _files.Last.Value;
+            await file.EnqueueAsync(entity);
+
+            var checkFileSize = !ReduceQueueFileSizeChecks;
+            if (ReduceQueueFileSizeChecks && DateTime.UtcNow.Subtract(_lastQueueFileSizeCheck).TotalMilliseconds > 500)
+            {
+                checkFileSize = true;
+                _lastQueueFileSizeCheck = DateTime.UtcNow;
+            }
+
+            _logger.Debug($"{_queueName} is storing {((Message)(object)entity).Body}");
+            if (checkFileSize && file.CurrentFileSize > MaximumFileSize)
+            {
+                await file.FlushWriteAsync();
+                await file.CloseWriteAsync();
+                file = await OpenNewFile();
+                _files.AddLast(file);
+            }
+            else if (AutoFlush)
+            {
+                await file.FlushWriteAsync();
+            }
+        }
+
+        /// 
+        ///     Flush everything from disk.
+        /// 
+        /// 
+        public async Task FlushAsync()
+        {
+            await _files.Last.Value.FlushWriteAsync();
+            await _dequeueTracking.FlushAsync();
+        }
+
+        /// 
+        ///     Open first queue file.
+        /// 
+        /// 
+        public async Task OpenAsync(TimeSpan timeout)
+        {
+            if (!Directory.Exists(_queueDirectory)) Directory.CreateDirectory(_queueDirectory);
+
+            _logger.Debug($"[{_queueName}] Opening queue..");
+
+            await _dequeueTracking.OpenAsync(timeout);
+
+            var lastRecordPosition = _dequeueTracking.LastReadRecord;
+            _lastQueueFileSizeCheck = DateTime.UtcNow;
+
+            _logger.Debug($"[{_queueName}] Loading files..");
+            await LoadFiles(lastRecordPosition);
+            if (_files.Count == 0)
+            {
+                var file = await OpenNewFile();
+                _files.AddLast(file);
+            }
+        }
+
+        private async Task LoadFiles(int lastReadRecordPosition)
+        {
+            var files = Directory
+                .GetFiles(_queueDirectory, _queueName + "_*.data")
+                .OrderBy(x => x)
+                .ToList();
+
+            var lastIndex = files.Count - 1;
+            for (var index = 0; index < files.Count; index++)
+            {
+                var file = files[index];
+
+                var queueFile = new DiskFile(_queueName, file);
+                if (lastReadRecordPosition >= 0)
+                {
+                    if (queueFile.CurrentFileSize < lastReadRecordPosition)
+                    {
+                        _logger.Error(
+                            $"Last record {lastReadRecordPosition} is larger than the file size {queueFile.CurrentFileSize}, we'll ignore this file: " +
+                            file);
+                        await queueFile.CloseWriteAsync();
+                        await queueFile.CloseReadAsync();
+                        await queueFile.Delete();
+                        continue;
+                    }
+
+                    await queueFile.OpenAsync(lastReadRecordPosition, index == lastIndex);
+                    lastReadRecordPosition = -1;
+                }
+                else
+                {
+                    await queueFile.OpenAsync(0, index == lastIndex);
+                }
+
+                _files.AddLast(queueFile);
+            }
+        }
+
+        private async Task MoveToNextQueueFile()
+        {
+            _logger.Info("Moving to next file");
+
+            await _dequeueTracking.ResetAsync();
+            var file = _files.First.Value;
+            await file.CloseReadAsync();
+            await file.Delete();
+            _files.RemoveFirst();
+        }
+
+        private async Task> OpenNewFile()
+        {
+            var name = Path.Combine(_queueDirectory,
+                $"{_queueName}_{DateTime.UtcNow:yyyyMMdHHmmssfff}.data");
+            var file = new DiskFile(_queueName, name) {Serializer = Serializer};
+            await file.OpenAsync(0, true);
+            return file;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/IContentSerializer.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/IContentSerializer.cs
new file mode 100644
index 00000000..fe57d14b
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/IContentSerializer.cs
@@ -0,0 +1,15 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Queue
+{
+    /// 
+    ///     Takes care of reading and writing to the queue data file.
+    /// 
+    public interface IContentSerializer
+    {
+        Task DeserializeAsync(Stream source, int recordSize, Type entityType);
+        Task SerializeAsync(Stream destination, object entity);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/ReadState.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/ReadState.cs
new file mode 100644
index 00000000..3efa3106
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/ReadState.cs
@@ -0,0 +1,243 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Queue
+{
+    /// 
+    ///     Reads from a disk file using on own internal cache when doing so.
+    /// 
+    /// 
+    internal class ReadState
+    {
+        private const int HeaderLength = 8;
+        private const int SizeHeaderLength = 4;
+        private const int RecordSeparatorLength = 4;
+        private readonly IContentSerializer _contentSerializer;
+        private readonly byte[] _recordSeparator;
+        private readonly Stream _sourceStream;
+        private byte[] _buffer;
+        private int _bufferBytesLeft;
+        private int _bufferOffset;
+
+        public ReadState(Stream sourceStream, int bufferSize, byte[] recordSeparator,
+            IContentSerializer contentSerializer)
+        {
+            _sourceStream = sourceStream;
+            _recordSeparator = recordSeparator;
+            _contentSerializer = contentSerializer;
+            _buffer = new byte[bufferSize];
+        }
+
+        public ReadState(Stream readStream, byte[] buffer)
+        {
+            _sourceStream = readStream;
+            _buffer = buffer;
+            _bufferOffset = 0;
+            _bufferBytesLeft = 0;
+        }
+
+        public async Task AdjustForPositionBug()
+        {
+            var beforeOffset = _sourceStream.Position;
+            var read = await _sourceStream.ReadAsync(_buffer, 0, HeaderLength);
+            if (read == 0) return;
+
+            if (read != HeaderLength) Debugger.Break();
+
+            if (_buffer[0] == 1 && _buffer[1] == 3 && _buffer[2] == 3 && _buffer[3] == 7)
+            {
+                _sourceStream.Position = beforeOffset;
+                return;
+            }
+
+            _sourceStream.Position = beforeOffset - HeaderLength;
+            read = await _sourceStream.ReadAsync(_buffer, 0, 8);
+            if (read == 0) return;
+
+            if (_buffer[0] == 1 && _buffer[1] == 3 && _buffer[2] == 3 && _buffer[3] == 7)
+            {
+                _sourceStream.Position = beforeOffset - HeaderLength;
+                return;
+            }
+
+            Debugger.Break();
+
+            // All failed, let's sweep.
+            _sourceStream.Position = beforeOffset - _buffer.Length / 2;
+            var startPosition = _sourceStream.Position;
+            await _sourceStream.ReadAsync(_buffer, 0, _buffer.Length);
+
+            var index = 0;
+            while (index < _buffer.Length)
+            {
+                if (_buffer[index + 0] != 1 || _buffer[index + 1] != 3 || _buffer[index + 2] != 3 ||
+                    _buffer[index + 3] != 7)
+                {
+                    index++;
+                    continue;
+                }
+
+                _sourceStream.Position = startPosition + index;
+                return;
+            }
+
+            Debugger.Break();
+        }
+
+        /// 
+        ///     Go through entire file looking for records. Resets state once done.
+        /// 
+        /// 
+        public async Task CountRecords()
+        {
+            var startPos = _sourceStream.Position;
+            var recordCount = 0;
+            while (true)
+            {
+                // find record 
+                while (true)
+                {
+                    var gotEnough = await EnsureEnoughBytes(8);
+                    if (!gotEnough)
+                    {
+                        // reset state before returning
+                        _sourceStream.Position = startPos;
+                        _bufferOffset = 0;
+                        _bufferBytesLeft = 0;
+                        return recordCount;
+                    }
+
+
+                    var isValidEntry = VerifyRecordSeparator(_buffer, _bufferOffset);
+                    if (!isValidEntry)
+                    {
+                        // move forward one so that we can loop until we find a correct record.
+                        _bufferOffset++;
+                        _bufferBytesLeft--;
+                        continue;
+                    }
+
+                    break;
+                }
+
+                var recordSize = BitConverter.ToInt32(_buffer, _bufferOffset + RecordSeparatorLength);
+
+                _bufferOffset += HeaderLength;
+                _bufferBytesLeft -= HeaderLength;
+
+                var gotBodyBytes = await EnsureEnoughBytes(recordSize);
+                if (!gotBodyBytes) throw new InvalidOperationException("Expected to find a complete body. Failed.");
+
+                _bufferOffset += recordSize;
+                _bufferBytesLeft -= recordSize;
+                recordCount++;
+            }
+        }
+
+        public bool MustFlushWrite(long lastFlushedWriteIndex)
+        {
+            return _sourceStream.Position + HeaderLength >= lastFlushedWriteIndex;
+        }
+
+        public async Task> ReadRecord()
+        {
+            var corruptRecordOffset = -1;
+
+            while (true)
+            {
+                var gotEnough = await EnsureEnoughBytes(HeaderLength);
+                if (!gotEnough)
+                    return null;
+
+                var isValidEntry = VerifyRecordSeparator(_buffer, _bufferOffset);
+                if (!isValidEntry)
+                {
+                    corruptRecordOffset = (int)_sourceStream.Position - _bufferBytesLeft;
+                    // move forward one so that we can loop until we find a correct record.
+                    // 
+                    _bufferOffset++;
+                    _bufferBytesLeft--;
+                    continue;
+                }
+
+                // yay got a valid record.
+                if (corruptRecordOffset > -1)
+                {
+                    //TODO: Report the record for analysis
+                }
+
+                break;
+            }
+
+            var recordSize = BitConverter.ToInt32(_buffer, _bufferOffset + RecordSeparatorLength);
+            var recordStartPosition = _sourceStream.Position - _bufferBytesLeft;
+
+            _bufferOffset += HeaderLength;
+            _bufferBytesLeft -= HeaderLength;
+
+            var gotBodyBytes = await EnsureEnoughBytes(recordSize);
+            if (!gotBodyBytes) throw new InvalidOperationException("Expected to find a complete body. Failed.");
+
+
+            var stream = new BufferStream(_buffer, _bufferOffset, recordSize);
+            var entity = (TEntity)await _contentSerializer.DeserializeAsync(stream, recordSize, typeof(TEntity));
+            _bufferOffset += recordSize;
+            _bufferBytesLeft -= recordSize;
+            return new Record(entity, (int)recordStartPosition);
+        }
+
+        private async Task EnsureEnoughBytes(int requestedAmountOfBytes)
+        {
+            if (_bufferBytesLeft > requestedAmountOfBytes)
+                return true;
+
+            var sourceOffset = _bufferOffset;
+
+            if (_bufferOffset + requestedAmountOfBytes > _buffer.Length)
+            {
+                if (requestedAmountOfBytes > _buffer.Length)
+                {
+                    var newBuffer = new byte[requestedAmountOfBytes * 2];
+                    Buffer.BlockCopy(_buffer, _bufferOffset, newBuffer, 0, _bufferBytesLeft);
+                    _buffer = newBuffer;
+                }
+                else
+                {
+                    Buffer.BlockCopy(_buffer, _bufferOffset, _buffer, 0, _bufferBytesLeft);
+                }
+
+                _bufferOffset = _bufferBytesLeft;
+                sourceOffset = 0;
+            }
+
+            var bytesLeftToRead = requestedAmountOfBytes - _bufferBytesLeft;
+            while (bytesLeftToRead > 0)
+            {
+                var bytesRead = await _sourceStream.ReadAsync(_buffer, _bufferOffset, _buffer.Length - _bufferOffset);
+                if (bytesRead == 0)
+                    return false;
+
+                bytesLeftToRead -= bytesRead;
+                _bufferOffset += bytesRead;
+                _bufferBytesLeft += bytesRead;
+            }
+
+            // Since we want to read from that position.
+            _bufferOffset = sourceOffset;
+            return true;
+        }
+
+        private bool VerifyRecordSeparator(byte[] buffer, int offset)
+        {
+            for (var i = 0; i < _recordSeparator.Length; i++)
+            {
+                if (_recordSeparator[i] != buffer[offset + i])
+                    return false;
+            }
+
+            return true;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/Record.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/Record.cs
new file mode 100644
index 00000000..71602cfb
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/Record.cs
@@ -0,0 +1,22 @@
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Queue
+{
+    /// 
+    /// 
+    /// 
+    public class Record
+    {
+        public Record(T entity, int recordOffset)
+        {
+            Entity = entity;
+            RecordOffset = recordOffset;
+        }
+
+        public T Entity { get; }
+
+        /// 
+        ///     Offset of our record (pointing at the header).
+        /// 
+        /// We had a bug where it pointed at the data, so when reading, check if we need to move backwards 8 bytes.
+        public int RecordOffset { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/SegmentedReadStream.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/SegmentedReadStream.cs
new file mode 100644
index 00000000..10ec84fb
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/Queue/SegmentedReadStream.cs
@@ -0,0 +1,121 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using log4net;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk.Queue
+{
+    /// 
+    ///     A stream with a shorter length than the actual stream.
+    /// 
+    /// 
+    ///     
+    ///         Some serializer can't use a segment from a stream. Therefore we need to fake the length of a stream to make
+    ///         sure that the serializer doesn't try to parse too much.
+    ///     
+    /// 
+    public class SegmentedReadStream : Stream
+    {
+        private readonly Stream _inner;
+        private int _bytesLeftToRead;
+        private ILog _logger = LogManager.GetLogger(typeof(SegmentedReadStream));
+        private int _segmentSize;
+
+        public SegmentedReadStream(Stream inner, int segmentSize)
+        {
+            _inner = inner ?? throw new ArgumentNullException(nameof(inner));
+            _segmentSize = segmentSize;
+        }
+
+        /// 
+        public override bool CanRead => _inner.CanRead;
+
+        /// 
+        public override bool CanSeek => _inner.CanSeek;
+
+        /// 
+        public override bool CanWrite => _inner.CanWrite;
+
+        /// 
+        public override long Length => _segmentSize;
+
+        /// 
+        public override long Position
+        {
+            get => _inner.Position;
+            set => _inner.Position = value;
+        }
+
+        /// 
+        public override void Flush()
+        {
+            _inner.Flush();
+        }
+
+        /// 
+        public override Task FlushAsync(CancellationToken cancellationToken)
+        {
+            return _inner.FlushAsync(cancellationToken);
+        }
+
+        /// 
+        public override int Read(byte[] buffer, int offset, int count)
+        {
+            if (_bytesLeftToRead < count) count = _bytesLeftToRead;
+
+            if (count == 0)
+                return 0;
+
+            var read = _inner.Read(buffer, offset, count);
+            _bytesLeftToRead -= read;
+            return read;
+        }
+
+        /// 
+        public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            if (offset + count > _segmentSize)
+                count = _segmentSize - offset;
+
+            return _inner.ReadAsync(buffer, offset, count, cancellationToken);
+        }
+
+        /// 
+        public override long Seek(long offset, SeekOrigin origin)
+        {
+            throw new NotSupportedException();
+        }
+
+        /// 
+        /// 
+        ///     Not supported, control the size through 
+        /// 
+        public override void SetLength(long value)
+        {
+            throw new NotSupportedException();
+        }
+
+        /// 
+        ///     Set the fake limit.
+        /// 
+        /// 
+        public void SetSegmentSize(int recordSize)
+        {
+            _segmentSize = recordSize;
+            _bytesLeftToRead = _segmentSize;
+        }
+
+        /// 
+        public override void Write(byte[] buffer, int offset, int count)
+        {
+            throw new NotSupportedException();
+        }
+
+        /// 
+        public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+        {
+            throw new NotSupportedException();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/QueueLockFile.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/QueueLockFile.cs
new file mode 100644
index 00000000..deb93a5f
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Disk/QueueLockFile.cs
@@ -0,0 +1,169 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.Infrastructure.Messaging.Disk
+{
+    /// 
+    ///     Used to make sure that only one process have access to the queue files to avoid synchronization issues.
+    /// 
+    public class QueueLockFile
+    {
+        private readonly string _lockFileName;
+        private readonly string _queueDirectory;
+        private readonly string _queueName;
+        private readonly string _releaseRequestFileName;
+        private bool _isOwnedByUs;
+        private FileStream _lockFile;
+        private Timer _timer;
+
+        public QueueLockFile(string queueDirectory, string queueName)
+        {
+            _queueDirectory = queueDirectory;
+            _queueName = queueName;
+            _lockFileName = Path.Combine(_queueDirectory, $"{_queueName}.lock");
+            _releaseRequestFileName = Path.Combine(_queueDirectory, $"{_queueName}-release.lock");
+        }
+
+        /// 
+        ///     Callback used to determine if the queues can be shutdown.
+        /// 
+        /// 
+        ///     The callback must also shutdown the queues before returning.
+        /// 
+        /// 
+        ///     true = the queues have been shutdown; false = cannot shutdown the queues currently.
+        /// 
+        public Func> CloseQueueRequested { get; set; }
+
+        /// 
+        ///     Last exception during the internal processing.
+        /// 
+        public Exception LastException { get; set; }
+
+
+        /// 
+        ///     Attempt to create the lock file.
+        /// 
+        /// 
+        /// 
+        /// Lock file has already been created (by this or another process).
+        public async Task CreateLockFile(TimeSpan timeout)
+        {
+            if (!File.Exists(_lockFileName))
+            {
+                LockQueue();
+                return;
+            }
+
+            // Try to delete it (app crashed)
+            try
+            {
+                File.Delete(_lockFileName);
+                File.Delete(_releaseRequestFileName);
+                LockQueue();
+                return;
+            }
+            catch (IOException)
+            {
+                //a
+            }
+
+            File.WriteAllText(_releaseRequestFileName, Base64Encode($"Release {_queueName}, please."));
+            var isFree = false;
+            var exitAt = DateTime.UtcNow.Add(timeout);
+            while (exitAt >= DateTime.UtcNow)
+            {
+                await Task.Delay(100);
+                if (!File.Exists(_lockFileName))
+                {
+                    isFree = true;
+                    break;
+                }
+            }
+
+            if (!isFree) throw new InvalidOperationException("Failed to allocate queue");
+
+            LockQueue();
+        }
+
+        public Task DeleteLockFile()
+        {
+            if (!_isOwnedByUs) throw new InvalidOperationException("File is not owned by us.");
+
+            _lockFile.Close();
+            File.Delete(_lockFileName);
+            File.Delete(_releaseRequestFileName);
+            _isOwnedByUs = false;
+            return Task.CompletedTask;
+        }
+
+        private static string Base64Encode(string plainText)
+        {
+            var plainTextBytes = Encoding.UTF8.GetBytes(plainText);
+            return Convert.ToBase64String(plainTextBytes);
+        }
+
+        private void LockQueue()
+        {
+            if (!Directory.Exists(_queueDirectory)) Directory.CreateDirectory(_queueDirectory);
+
+            File.WriteAllText(_lockFileName, $"Locked since {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
+            _lockFile = new FileStream(_lockFileName, FileMode.Append, FileAccess.Write, FileShare.Read);
+            _isOwnedByUs = true;
+            _timer = new Timer(OnCheckForReleaseRequests);
+            _timer.Change(100, 100);
+        }
+
+        /// 
+        ///     Check if a new process is requesting to get the files.
+        /// 
+        /// 
+        /// 
+        ///     
+        ///         IIS application pool recycles will always spin up a new process before shutting down the previous process.
+        ///         When writing file based queues that means that the new process can't get access to the files during this
+        ///         overlapping period.
+        ///     
+        ///     
+        ///         To solve that, the new process will request that the old process releases/closes the queues before it has been
+        ///         completely shutdown. This method
+        ///         checks for that request file, checks if it's OK to shutdown and then do so.
+        ///     
+        /// 
+        private void OnCheckForReleaseRequests(object state)
+        {
+            var expectedString = Base64Encode($"Release {_queueName}, please.");
+            if (!File.Exists(_releaseRequestFileName)) return;
+
+            try
+            {
+                var actualString = File.ReadAllText(_releaseRequestFileName);
+                if (actualString != expectedString)
+                {
+                    File.Delete(_releaseRequestFileName);
+                    return;
+                }
+
+                if (CloseQueueRequested == null) return;
+
+                _timer.Change(Timeout.Infinite, Timeout.Infinite);
+                var canShutDown = CloseQueueRequested().GetAwaiter().GetResult();
+                if (!canShutDown)
+                {
+                    _timer.Change(100, 100);
+                    return;
+                }
+
+                // Not owned by us if we have been stopped by another thread.
+                if (_isOwnedByUs) DeleteLockFile();
+            }
+            catch (Exception ex)
+            {
+                LastException = ex;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/IRouteRegistrar.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/IRouteRegistrar.cs
new file mode 100644
index 00000000..0f72fdd1
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/IRouteRegistrar.cs
@@ -0,0 +1,22 @@
+using DotNetCqs.Queues;
+
+namespace Coderr.Server.Infrastructure.Messaging
+{
+    /// 
+    ///     Register a queue.
+    /// 
+    public interface IRouteRegistrar
+    {
+        /// 
+        ///     Queue for application (i.e. business specific) messages (all but the ReportAnalyzer namespaces).
+        /// 
+        /// 
+        void RegisterAppQueue(IMessageQueue queue);
+
+        /// 
+        ///     Queue for the report analyzer pipeline (the report analyzer namespaces).
+        /// 
+        /// 
+        void RegisterReportQueue(IMessageQueue queue);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/IncludeNonPublicAndUseCamelCaseContractResolver.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/IncludeNonPublicAndUseCamelCaseContractResolver.cs
new file mode 100644
index 00000000..bc05e3e5
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/IncludeNonPublicAndUseCamelCaseContractResolver.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace Coderr.Server.Infrastructure.Messaging
+{
+    /// 
+    ///     Used by JSON.NET to be able to deserialize properties with private setters.
+    /// 
+    public class IncludeNonPublicMembersContractResolver : DefaultContractResolver
+    {
+        //protected override List GetSerializableMembers(Type objectType)
+        //{
+        //    var members = base.GetSerializableMembers(objectType);
+        //    return members.Where(m => !m.Name.EndsWith("k__BackingField")).ToList();
+        //}
+
+        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
+        {
+            //TODO: Maybe cache
+            var prop = base.CreateProperty(member, memberSerialization);
+
+            if (!prop.Writable)
+            {
+                var property = member as PropertyInfo;
+                if (property != null)
+                {
+                    var hasPrivateSetter = property.GetSetMethod(true) != null;
+                    prop.Writable = hasPrivateSetter;
+                }
+            }
+
+            return prop;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/MessageRouter.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/MessageRouter.cs
new file mode 100644
index 00000000..06670fac
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/MessageRouter.cs
@@ -0,0 +1,186 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Coderr.Client;
+using Coderr.Server.Abstractions.Security;
+using DotNetCqs;
+using DotNetCqs.Queues;
+using log4net;
+
+namespace Coderr.Server.Infrastructure.Messaging
+{
+    /// 
+    ///     Used to decide which queue outbound messages should be enqueued in.
+    /// 
+    public class MessageRouter : IMessageRouter, IRouteRegistrar
+    {
+        public static readonly MessageRouter Instance = new MessageRouter();
+        private readonly List _appQueues = new List();
+        private readonly ILog _logger = LogManager.GetLogger(typeof(MessageRouter));
+        private readonly List _reportAnalyzerQueues = new List();
+
+        private MessageRouter()
+        {
+        }
+
+        async Task IMessageRouter.SendAsync(Message message)
+        {
+            await SendAsync(null, message);
+        }
+
+
+        async Task IMessageRouter.SendAsync(IReadOnlyCollection messages)
+        {
+            await SendAsync(null, messages);
+        }
+
+        public async Task SendAsync(ClaimsPrincipal principal, Message message)
+        {
+            if (!IsReportAnalyzerMessage(message))
+                await SendAppMessage(principal, message);
+            else
+                await SendReportAnalyzerMessage(principal, message);
+        }
+
+        public async Task SendAsync(ClaimsPrincipal principal, IReadOnlyCollection messages)
+        {
+            var reporAnalyzerMessages = messages.Where(IsReportAnalyzerMessage).ToList();
+            var appMsgs = messages.Except(reporAnalyzerMessages).ToList();
+            await SendReportAnalyzerMessages(principal, reporAnalyzerMessages);
+            await SendAppMessages(principal, appMsgs);
+        }
+
+        public void RegisterAppQueue(IMessageQueue queue)
+        {
+            if (queue == null) throw new ArgumentNullException(nameof(queue));
+
+            // We have multiple places now that can register a queue.
+            // TODO: Clean that up ;)
+            if (_appQueues.Any(existingQueue => existingQueue.Name == queue.Name)) return;
+
+            _appQueues.Add(queue);
+        }
+
+        public void RegisterReportQueue(IMessageQueue queue)
+        {
+            if (queue == null) throw new ArgumentNullException(nameof(queue));
+
+            // We have multiple places now that can register a queue.
+            // TODO: Clean that up ;)
+            if (_reportAnalyzerQueues.Any(existingQueue => existingQueue.Name == queue.Name)) return;
+
+            _reportAnalyzerQueues.Add(queue);
+        }
+
+        private static bool IsReportAnalyzerMessage(Message message)
+        {
+            return message?.Body?.GetType().Namespace?.Contains("ReportAnalyzer") == true;
+        }
+
+        private async Task SendAppMessage(ClaimsPrincipal claimsPrincipal, Message message)
+        {
+            if (claimsPrincipal == null)
+                _logger.Warn("Null principal for " + message.Body.GetType() + " from " + Environment.StackTrace);
+
+            foreach (var queue in _appQueues)
+            {
+                try
+                {
+                    using (var session = queue.BeginSession())
+                    {
+                        if (claimsPrincipal == null)
+                            await session.EnqueueAsync(message);
+                        else
+                            await session.EnqueueAsync(claimsPrincipal, message);
+                        _logger.Info($"AppMsg[{claimsPrincipal?.ToFriendlyString()}]: {message.Body}");
+                        await session.SaveChanges();
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Err.Report(ex, new {queue.Name});
+                }
+            }
+        }
+
+        private async Task SendAppMessages(ClaimsPrincipal principal, List msgs)
+        {
+            if (principal == null)
+                _logger.Warn("Null principal for " + msgs.FirstOrDefault()?.Body.GetType() + " from " +
+                             Environment.StackTrace);
+
+
+            foreach (var queue in _appQueues)
+            {
+                try
+                {
+                    using (var session = queue.BeginSession())
+                    {
+                        foreach (var message in msgs)
+                            _logger.Debug($"AppMsg[{principal?.ToFriendlyString()}]: {message.Body}");
+                        await session.EnqueueAsync(principal, msgs);
+                        await session.SaveChanges();
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Err.Report(ex, new {queue.Name});
+                }
+            }
+        }
+
+        private async Task SendReportAnalyzerMessage(ClaimsPrincipal claimsPrincipal, Message message)
+        {
+            if (claimsPrincipal == null)
+                _logger.Warn("Null principal for " + message.Body.GetType() + " from " + Environment.StackTrace);
+
+
+            foreach (var queue in _reportAnalyzerQueues)
+            {
+                try
+                {
+                    using (var session = queue.BeginSession())
+                    {
+                        if (claimsPrincipal == null)
+                            await session.EnqueueAsync(message);
+                        else
+                            await session.EnqueueAsync(claimsPrincipal, message);
+                        _logger.Debug($"AnalyzerMsg[{claimsPrincipal?.ToFriendlyString()}]: {message.Body}");
+                        await session.SaveChanges();
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Err.Report(ex, new {queue.Name});
+                }
+            }
+        }
+
+        private async Task SendReportAnalyzerMessages(ClaimsPrincipal principal, List msgs)
+        {
+            if (principal == null)
+                _logger.Warn("Null principal for " + msgs.FirstOrDefault()?.Body.GetType() + " from " +
+                             Environment.StackTrace);
+
+            foreach (var queue in _reportAnalyzerQueues)
+            {
+                try
+                {
+                    using (var session = queue.BeginSession())
+                    {
+                        foreach (var message in msgs)
+                            _logger.Debug($"AnalyzerMsg[{principal?.ToFriendlyString()}]: {message.Body}");
+                        await session.EnqueueAsync(principal, msgs);
+                        await session.SaveChanges();
+                    }
+                }
+                catch (Exception ex)
+                {
+                    Err.Report(ex, new {queue.Name});
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/MessagingSerializer.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/MessagingSerializer.cs
new file mode 100644
index 00000000..93cb0609
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/MessagingSerializer.cs
@@ -0,0 +1,94 @@
+using System;
+using DotNetCqs.Queues;
+using log4net;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Coderr.Server.Infrastructure.Messaging
+{
+    /// 
+    ///     This serializer is used for queueing and may not expose internal type definitions.
+    /// 
+    public class MessagingSerializer : IMessageSerializer
+    {
+        private readonly Type _messageEnvelope;
+
+        private readonly JsonSerializerSettings _settings = new JsonSerializerSettings
+        {
+            NullValueHandling = NullValueHandling.Ignore,
+            TypeNameHandling = TypeNameHandling.None,
+            ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
+            ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor,
+            ContractResolver = new IncludeNonPublicMembersContractResolver()
+        };
+
+        private readonly ILog _logger = LogManager.GetLogger(typeof(MessagingSerializer));
+
+
+        public MessagingSerializer()
+        {
+            _settings.Converters.Add(new StringEnumConverter());
+        }
+
+        public MessagingSerializer(Type messageEnvelope)
+        {
+            _messageEnvelope = messageEnvelope;
+            _settings.Converters.Add(new StringEnumConverter());
+        }
+
+        public bool ThrowExceptionOnDeserialziationFailure { get; set; } = true;
+
+        object IMessageSerializer.Deserialize(string contentType, string serializedDto)
+        {
+            try
+            {
+                var type = contentType == "Message"
+                    ? _messageEnvelope
+                    : Type.GetType(contentType);
+
+                if (type == null)
+                {
+                    if (ThrowExceptionOnDeserialziationFailure)
+                        throw new SerializationException($"Failed to lookup type \'{contentType}\'.", serializedDto);
+
+                    _logger.Error($"Invalid message type. throwing away message. {contentType}");
+                    return null;
+                }
+
+
+                return JsonConvert.DeserializeObject(serializedDto, type, _settings);
+            }
+            catch (JsonException ex)
+            {
+                if (ThrowExceptionOnDeserialziationFailure)
+                    throw new SerializationException($"Failed to deserialize \'{contentType}\'.", serializedDto, ex);
+                _logger.Error($"Failed to deserialize \'{contentType}\'.");
+                return null;
+            }
+        }
+
+        void IMessageSerializer.Serialize(object dto, out string serializedDto, out string contentType)
+        {
+            try
+            {
+                contentType = dto.GetType().AssemblyQualifiedName;
+                serializedDto = JsonConvert.SerializeObject(dto, _settings);
+            }
+            catch (JsonException ex)
+            {
+                throw new SerializationException($"Failed to serialize \'{dto.GetType().FullName}\'.", dto, ex);
+            }
+        }
+
+        public object Deserialize(Type type, string message)
+        {
+            return JsonConvert.DeserializeObject(message, type, _settings);
+        }
+
+        public string Serialize(object message, out string contentType)
+        {
+            contentType = "application/json";
+            return JsonConvert.SerializeObject(message, _settings);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/QueueManager.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/QueueManager.cs
new file mode 100644
index 00000000..8ded2552
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/QueueManager.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Data;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Infrastructure.Messaging.Disk.DotNetCqs;
+using Coderr.Server.Infrastructure.Messaging.Tests;
+using DotNetCqs.Queues;
+using DotNetCqs.Queues.AdoNet;
+
+namespace Coderr.Server.Infrastructure.Messaging
+{
+    public class QueueManager : IDisposable
+    {
+        public static readonly TestQueueProvider TestProvider = new TestQueueProvider();
+
+        public static QueueManager Instance { get; private set; }
+
+        public IMessageQueueProvider QueueProvider { get; private set; }
+
+        public bool UseDiskQueues { get; private set; }
+
+        public void Dispose()
+        {
+            if (QueueProvider is IDisposable d)
+                d.Dispose();
+        }
+
+        public static void UseTestProvider()
+        {
+            Instance = new QueueManager {QueueProvider = TestProvider};
+        }
+
+        public void Configure(IConfiguration configuration, Func connectionFactory)
+        {
+            Instance = this;
+            UseDiskQueues = configuration.GetSection("Queues")["Type"] == "Disk";
+
+            if (UseDiskQueues)
+            {
+                var f1 = AppDomain.CurrentDomain.GetData("DataDirectory");
+                var folder = configuration.GetSection("Queues")["Folder"];
+                var diskProvider = new DiskQueueProvider(folder);
+                diskProvider.ShutdownRequested = OnInnerShutdownRequested;
+                QueueProvider = diskProvider;
+            }
+            else
+            {
+                IDbConnection Factory()
+                {
+                    return connectionFactory(CoderrClaims.SystemPrincipal);
+                }
+
+                var serializer = new MessagingSerializer(typeof(AdoNetMessageDto));
+                QueueProvider = new AdoNetMessageQueueProvider(Factory, serializer);
+            }
+        }
+
+        public IMessageQueue GetQueue(string queueName)
+        {
+            if (QueueProvider == null)
+                throw new InvalidOperationException("Must configure first.");
+
+            return QueueProvider.Open(queueName);
+        }
+
+        public void SetCustomProvider(IMessageQueueProvider provider)
+        {
+            QueueProvider = provider ?? throw new ArgumentNullException(nameof(provider));
+        }
+
+        /// 
+        /// 
+        public event EventHandler ShutdownRequestCompleted;
+
+        /// 
+        ///     We have been requested to shut down.
+        /// 
+        public event EventHandler ShutdownRequested;
+
+        private Task OnInnerShutdownRequested()
+        {
+            var e = new ShuttingDownEventArgs();
+            ShutdownRequested?.Invoke(this, e);
+
+            var e2 = new ShutdownRequestCompletedEventArgs(e.CanShutdown);
+            ShutdownRequestCompleted?.Invoke(this, e2);
+
+            if (e.CanShutdown) Dispose();
+
+            return Task.FromResult(e.CanShutdown);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/ShutdownRequestCompletedEventArgs.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/ShutdownRequestCompletedEventArgs.cs
new file mode 100644
index 00000000..e48b95a9
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/ShutdownRequestCompletedEventArgs.cs
@@ -0,0 +1,12 @@
+namespace Coderr.Server.Infrastructure.Messaging
+{
+    public class ShutdownRequestCompletedEventArgs
+    {
+        public ShutdownRequestCompletedEventArgs(bool isShutDown)
+        {
+            IsShutDown = isShutDown;
+        }
+
+        public bool IsShutDown { get; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/ShuttingDownEventArgs.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/ShuttingDownEventArgs.cs
new file mode 100644
index 00000000..6e73f676
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/ShuttingDownEventArgs.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Coderr.Server.Infrastructure.Messaging
+{
+    public class ShuttingDownEventArgs : EventArgs
+    {
+        public bool CanShutdown { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestMessageWrapper.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestMessageWrapper.cs
new file mode 100644
index 00000000..53a4eb24
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestMessageWrapper.cs
@@ -0,0 +1,17 @@
+using System.Security.Claims;
+using DotNetCqs;
+
+namespace Coderr.Server.Infrastructure.Messaging.Tests
+{
+    public class TestMessageWrapper
+    {
+        public TestMessageWrapper(ClaimsPrincipal principal, Message message)
+        {
+            Principal = principal;
+            Message = message;
+        }
+
+        public Message Message { get; }
+        public ClaimsPrincipal Principal { get; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueue.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueue.cs
new file mode 100644
index 00000000..ae359169
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueue.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using DotNetCqs.Queues;
+
+namespace Coderr.Server.Infrastructure.Messaging.Tests
+{
+    public class TestQueue : IMessageQueue
+    {
+        public List Sessions = new List();
+        public Queue SessionsToReturn = new Queue();
+
+        public TestQueue(string queueName)
+        {
+            Name = queueName;
+        }
+
+        public bool HasBegun { get; set; }
+
+        public IMessageQueueSession BeginSession()
+        {
+            var session = SessionsToReturn.Count > 0 ? SessionsToReturn.Dequeue() : new TestQueueSession();
+
+            Sessions.Add(session);
+            return session;
+        }
+
+        public TestMessageWrapper Dequeue()
+        {
+            if (Sessions.Count == 0)
+                return null;
+
+            var session = Sessions.First(x => x.EnqueuedCount > 0);
+            var msg = session.Enqueued.Dequeue();
+            return msg;
+        }
+
+        public string Name { get; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueueProvider.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueueProvider.cs
new file mode 100644
index 00000000..d20a368f
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueueProvider.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using DotNetCqs.Queues;
+
+namespace Coderr.Server.Infrastructure.Messaging.Tests
+{
+    public class TestQueueProvider : IMessageQueueProvider
+    {
+        private Dictionary _queues = new Dictionary();
+
+        public IMessageQueue Open(string queueName)
+        {
+            if (!_queues.TryGetValue(queueName, out var queue))
+            {
+                queue = new TestQueue(queueName);
+                _queues[queueName] = queue;
+            }
+
+
+            return queue;
+        }
+
+        public TestQueue this[string name] => _queues[name];
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueueSession.cs b/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueueSession.cs
new file mode 100644
index 00000000..99ed32d6
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Messaging/Tests/TestQueueSession.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using DotNetCqs;
+using DotNetCqs.Queues;
+
+namespace Coderr.Server.Infrastructure.Messaging.Tests
+{
+    public class TestQueueSession : IMessageQueueSession
+    {
+        private readonly Queue _enqueued = new Queue();
+        private readonly Queue _dequeued = new Queue();
+
+        public bool IsDisposed { get; private set; }
+
+        public bool IsSaved { get; private set; }
+        public int EnqueuedCount => _enqueued.Count;
+
+        public Queue Enqueued => _enqueued;
+
+        public void Dispose()
+        {
+            IsDisposed = true;
+        }
+
+        public Task Dequeue(TimeSpan suggestedWaitPeriod)
+        {
+            return Task.FromResult(_dequeued.Dequeue()?.Message);
+        }
+
+        public Task DequeueWithCredentials(TimeSpan suggestedWaitPeriod)
+        {
+            var message = _dequeued.Dequeue();
+            return message == null
+                ? null
+                : Task.FromResult(new DequeuedMessage(message.Principal, message.Message));
+        }
+
+        public Task EnqueueAsync(ClaimsPrincipal principal, IReadOnlyCollection messages)
+        {
+            foreach (var message in messages)
+            {
+                _enqueued.Enqueue(new TestMessageWrapper(principal, message));
+            }
+
+            return Task.CompletedTask;
+        }
+
+        public Task EnqueueAsync(IReadOnlyCollection messages)
+        {
+            foreach (var message in messages) _enqueued.Enqueue(new TestMessageWrapper(null, message));
+
+            return Task.CompletedTask;
+        }
+
+        public Task EnqueueAsync(ClaimsPrincipal principal, Message message)
+        {
+            _enqueued.Enqueue(new TestMessageWrapper(principal, message));
+            return Task.CompletedTask;
+        }
+
+        public Task EnqueueAsync(Message message)
+        {
+            _enqueued.Enqueue(new TestMessageWrapper(null, message));
+            return Task.CompletedTask;
+        }
+
+        public Task SaveChanges()
+        {
+            IsSaved = true;
+            return Task.CompletedTask;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.Data.Common/Net/MimeMapping.cs b/src/Server/Coderr.Server.Infrastructure/Net/MimeMapping.cs
similarity index 97%
rename from src/Server/OneTrueError.Data.Common/Net/MimeMapping.cs
rename to src/Server/Coderr.Server.Infrastructure/Net/MimeMapping.cs
index 31d9759e..e8371506 100644
--- a/src/Server/OneTrueError.Data.Common/Net/MimeMapping.cs
+++ b/src/Server/Coderr.Server.Infrastructure/Net/MimeMapping.cs
@@ -1,604 +1,604 @@
-using System;
-using System.Collections.Generic;
-
-namespace OneTrueError.Infrastructure.Net
-{
-    /// 
-    ///     Mappings between file extensions and mime types
-    /// 
-    public static class MimeMapping
-    {
-        private static readonly IDictionary Mappings =
-            new Dictionary(StringComparer.InvariantCultureIgnoreCase)
-            {
-                #region Big freaking list of mime types
-
-                // combination of values from Windows 7 Registry and 
-                // from C:\Windows\System32\inetsrv\config\applicationHost.config
-                // some added, including .7z and .dat
-                {".323", "text/h323"},
-                {".3g2", "video/3gpp2"},
-                {".3gp", "video/3gpp"},
-                {".3gp2", "video/3gpp2"},
-                {".3gpp", "video/3gpp"},
-                {".7z", "application/x-7z-compressed"},
-                {".aa", "audio/audible"},
-                {".AAC", "audio/aac"},
-                {".aaf", "application/octet-stream"},
-                {".aax", "audio/vnd.audible.aax"},
-                {".ac3", "audio/ac3"},
-                {".aca", "application/octet-stream"},
-                {".accda", "application/msaccess.addin"},
-                {".accdb", "application/msaccess"},
-                {".accdc", "application/msaccess.cab"},
-                {".accde", "application/msaccess"},
-                {".accdr", "application/msaccess.runtime"},
-                {".accdt", "application/msaccess"},
-                {".accdw", "application/msaccess.webapplication"},
-                {".accft", "application/msaccess.ftemplate"},
-                {".acx", "application/internet-property-stream"},
-                {".AddIn", "text/xml"},
-                {".ade", "application/msaccess"},
-                {".adobebridge", "application/x-bridge-url"},
-                {".adp", "application/msaccess"},
-                {".ADT", "audio/vnd.dlna.adts"},
-                {".ADTS", "audio/aac"},
-                {".afm", "application/octet-stream"},
-                {".ai", "application/postscript"},
-                {".aif", "audio/x-aiff"},
-                {".aifc", "audio/aiff"},
-                {".aiff", "audio/aiff"},
-                {".air", "application/vnd.adobe.air-application-installer-package+zip"},
-                {".amc", "application/x-mpeg"},
-                {".application", "application/x-ms-application"},
-                {".art", "image/x-jg"},
-                {".asa", "application/xml"},
-                {".asax", "application/xml"},
-                {".ascx", "application/xml"},
-                {".asd", "application/octet-stream"},
-                {".asf", "video/x-ms-asf"},
-                {".ashx", "application/xml"},
-                {".asi", "application/octet-stream"},
-                {".asm", "text/plain"},
-                {".asmx", "application/xml"},
-                {".aspx", "application/xml"},
-                {".asr", "video/x-ms-asf"},
-                {".asx", "video/x-ms-asf"},
-                {".atom", "application/atom+xml"},
-                {".au", "audio/basic"},
-                {".avi", "video/x-msvideo"},
-                {".axs", "application/olescript"},
-                {".bas", "text/plain"},
-                {".bcpio", "application/x-bcpio"},
-                {".bin", "application/octet-stream"},
-                {".bmp", "image/bmp"},
-                {".c", "text/plain"},
-                {".cab", "application/octet-stream"},
-                {".caf", "audio/x-caf"},
-                {".calx", "application/vnd.ms-office.calx"},
-                {".cat", "application/vnd.ms-pki.seccat"},
-                {".cc", "text/plain"},
-                {".cd", "text/plain"},
-                {".cdda", "audio/aiff"},
-                {".cdf", "application/x-cdf"},
-                {".cer", "application/x-x509-ca-cert"},
-                {".chm", "application/octet-stream"},
-                {".class", "application/x-java-applet"},
-                {".clp", "application/x-msclip"},
-                {".cmx", "image/x-cmx"},
-                {".cnf", "text/plain"},
-                {".cod", "image/cis-cod"},
-                {".config", "application/xml"},
-                {".contact", "text/x-ms-contact"},
-                {".coverage", "application/xml"},
-                {".cpio", "application/x-cpio"},
-                {".cpp", "text/plain"},
-                {".crd", "application/x-mscardfile"},
-                {".crl", "application/pkix-crl"},
-                {".crt", "application/x-x509-ca-cert"},
-                {".cs", "text/plain"},
-                {".csdproj", "text/plain"},
-                {".csh", "application/x-csh"},
-                {".csproj", "text/plain"},
-                {".css", "text/css"},
-                {".csv", "text/csv"},
-                {".cur", "application/octet-stream"},
-                {".cxx", "text/plain"},
-                {".dat", "application/octet-stream"},
-                {".datasource", "application/xml"},
-                {".dbproj", "text/plain"},
-                {".dcr", "application/x-director"},
-                {".def", "text/plain"},
-                {".deploy", "application/octet-stream"},
-                {".der", "application/x-x509-ca-cert"},
-                {".dgml", "application/xml"},
-                {".dib", "image/bmp"},
-                {".dif", "video/x-dv"},
-                {".dir", "application/x-director"},
-                {".disco", "text/xml"},
-                {".dll", "application/x-msdownload"},
-                {".dll.config", "text/xml"},
-                {".dlm", "text/dlm"},
-                {".doc", "application/msword"},
-                {".docm", "application/vnd.ms-word.document.macroEnabled.12"},
-                {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
-                {".dot", "application/msword"},
-                {".dotm", "application/vnd.ms-word.template.macroEnabled.12"},
-                {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"},
-                {".dsp", "application/octet-stream"},
-                {".dsw", "text/plain"},
-                {".dtd", "text/xml"},
-                {".dtsConfig", "text/xml"},
-                {".dv", "video/x-dv"},
-                {".dvi", "application/x-dvi"},
-                {".dwf", "drawing/x-dwf"},
-                {".dwp", "application/octet-stream"},
-                {".dxr", "application/x-director"},
-                {".eml", "message/rfc822"},
-                {".emz", "application/octet-stream"},
-                {".eot", "application/octet-stream"},
-                {".eps", "application/postscript"},
-                {".etl", "application/etl"},
-                {".etx", "text/x-setext"},
-                {".evy", "application/envoy"},
-                {".exe", "application/octet-stream"},
-                {".exe.config", "text/xml"},
-                {".fdf", "application/vnd.fdf"},
-                {".fif", "application/fractals"},
-                {".filters", "Application/xml"},
-                {".fla", "application/octet-stream"},
-                {".flr", "x-world/x-vrml"},
-                {".flv", "video/x-flv"},
-                {".fsscript", "application/fsharp-script"},
-                {".fsx", "application/fsharp-script"},
-                {".generictest", "application/xml"},
-                {".gif", "image/gif"},
-                {".group", "text/x-ms-group"},
-                {".gsm", "audio/x-gsm"},
-                {".gtar", "application/x-gtar"},
-                {".gz", "application/x-gzip"},
-                {".h", "text/plain"},
-                {".hdf", "application/x-hdf"},
-                {".hdml", "text/x-hdml"},
-                {".hhc", "application/x-oleobject"},
-                {".hhk", "application/octet-stream"},
-                {".hhp", "application/octet-stream"},
-                {".hlp", "application/winhlp"},
-                {".hpp", "text/plain"},
-                {".hqx", "application/mac-binhex40"},
-                {".hta", "application/hta"},
-                {".htc", "text/x-component"},
-                {".htm", "text/html"},
-                {".html", "text/html"},
-                {".htt", "text/webviewhtml"},
-                {".hxa", "application/xml"},
-                {".hxc", "application/xml"},
-                {".hxd", "application/octet-stream"},
-                {".hxe", "application/xml"},
-                {".hxf", "application/xml"},
-                {".hxh", "application/octet-stream"},
-                {".hxi", "application/octet-stream"},
-                {".hxk", "application/xml"},
-                {".hxq", "application/octet-stream"},
-                {".hxr", "application/octet-stream"},
-                {".hxs", "application/octet-stream"},
-                {".hxt", "text/html"},
-                {".hxv", "application/xml"},
-                {".hxw", "application/octet-stream"},
-                {".hxx", "text/plain"},
-                {".i", "text/plain"},
-                {".ico", "image/x-icon"},
-                {".ics", "application/octet-stream"},
-                {".idl", "text/plain"},
-                {".ief", "image/ief"},
-                {".iii", "application/x-iphone"},
-                {".inc", "text/plain"},
-                {".inf", "application/octet-stream"},
-                {".inl", "text/plain"},
-                {".ins", "application/x-internet-signup"},
-                {".ipa", "application/x-itunes-ipa"},
-                {".ipg", "application/x-itunes-ipg"},
-                {".ipproj", "text/plain"},
-                {".ipsw", "application/x-itunes-ipsw"},
-                {".iqy", "text/x-ms-iqy"},
-                {".isp", "application/x-internet-signup"},
-                {".ite", "application/x-itunes-ite"},
-                {".itlp", "application/x-itunes-itlp"},
-                {".itms", "application/x-itunes-itms"},
-                {".itpc", "application/x-itunes-itpc"},
-                {".IVF", "video/x-ivf"},
-                {".jar", "application/java-archive"},
-                {".java", "application/octet-stream"},
-                {".jck", "application/liquidmotion"},
-                {".jcz", "application/liquidmotion"},
-                {".jfif", "image/pjpeg"},
-                {".jnlp", "application/x-java-jnlp-file"},
-                {".jpb", "application/octet-stream"},
-                {".jpe", "image/jpeg"},
-                {".jpeg", "image/jpeg"},
-                {".jpg", "image/jpeg"},
-                {".js", "application/x-javascript"},
-                {".jsx", "text/jscript"},
-                {".jsxbin", "text/plain"},
-                {".latex", "application/x-latex"},
-                {".library-ms", "application/windows-library+xml"},
-                {".lit", "application/x-ms-reader"},
-                {".loadtest", "application/xml"},
-                {".lpk", "application/octet-stream"},
-                {".lsf", "video/x-la-asf"},
-                {".lst", "text/plain"},
-                {".lsx", "video/x-la-asf"},
-                {".lzh", "application/octet-stream"},
-                {".m13", "application/x-msmediaview"},
-                {".m14", "application/x-msmediaview"},
-                {".m1v", "video/mpeg"},
-                {".m2t", "video/vnd.dlna.mpeg-tts"},
-                {".m2ts", "video/vnd.dlna.mpeg-tts"},
-                {".m2v", "video/mpeg"},
-                {".m3u", "audio/x-mpegurl"},
-                {".m3u8", "audio/x-mpegurl"},
-                {".m4a", "audio/m4a"},
-                {".m4b", "audio/m4b"},
-                {".m4p", "audio/m4p"},
-                {".m4r", "audio/x-m4r"},
-                {".m4v", "video/x-m4v"},
-                {".mac", "image/x-macpaint"},
-                {".mak", "text/plain"},
-                {".man", "application/x-troff-man"},
-                {".manifest", "application/x-ms-manifest"},
-                {".map", "text/plain"},
-                {".master", "application/xml"},
-                {".mda", "application/msaccess"},
-                {".mdb", "application/x-msaccess"},
-                {".mde", "application/msaccess"},
-                {".mdp", "application/octet-stream"},
-                {".me", "application/x-troff-me"},
-                {".mfp", "application/x-shockwave-flash"},
-                {".mht", "message/rfc822"},
-                {".mhtml", "message/rfc822"},
-                {".mid", "audio/mid"},
-                {".midi", "audio/mid"},
-                {".mix", "application/octet-stream"},
-                {".mk", "text/plain"},
-                {".mmf", "application/x-smaf"},
-                {".mno", "text/xml"},
-                {".mny", "application/x-msmoney"},
-                {".mod", "video/mpeg"},
-                {".mov", "video/quicktime"},
-                {".movie", "video/x-sgi-movie"},
-                {".mp2", "video/mpeg"},
-                {".mp2v", "video/mpeg"},
-                {".mp3", "audio/mpeg"},
-                {".mp4", "video/mp4"},
-                {".mp4v", "video/mp4"},
-                {".mpa", "video/mpeg"},
-                {".mpe", "video/mpeg"},
-                {".mpeg", "video/mpeg"},
-                {".mpf", "application/vnd.ms-mediapackage"},
-                {".mpg", "video/mpeg"},
-                {".mpp", "application/vnd.ms-project"},
-                {".mpv2", "video/mpeg"},
-                {".mqv", "video/quicktime"},
-                {".ms", "application/x-troff-ms"},
-                {".msi", "application/octet-stream"},
-                {".mso", "application/octet-stream"},
-                {".mts", "video/vnd.dlna.mpeg-tts"},
-                {".mtx", "application/xml"},
-                {".mvb", "application/x-msmediaview"},
-                {".mvc", "application/x-miva-compiled"},
-                {".mxp", "application/x-mmxp"},
-                {".nc", "application/x-netcdf"},
-                {".nsc", "video/x-ms-asf"},
-                {".nws", "message/rfc822"},
-                {".ocx", "application/octet-stream"},
-                {".oda", "application/oda"},
-                {".odc", "text/x-ms-odc"},
-                {".odh", "text/plain"},
-                {".odl", "text/plain"},
-                {".odp", "application/vnd.oasis.opendocument.presentation"},
-                {".ods", "application/oleobject"},
-                {".odt", "application/vnd.oasis.opendocument.text"},
-                {".one", "application/onenote"},
-                {".onea", "application/onenote"},
-                {".onepkg", "application/onenote"},
-                {".onetmp", "application/onenote"},
-                {".onetoc", "application/onenote"},
-                {".onetoc2", "application/onenote"},
-                {".orderedtest", "application/xml"},
-                {".osdx", "application/opensearchdescription+xml"},
-                {".p10", "application/pkcs10"},
-                {".p12", "application/x-pkcs12"},
-                {".p7b", "application/x-pkcs7-certificates"},
-                {".p7c", "application/pkcs7-mime"},
-                {".p7m", "application/pkcs7-mime"},
-                {".p7r", "application/x-pkcs7-certreqresp"},
-                {".p7s", "application/pkcs7-signature"},
-                {".pbm", "image/x-portable-bitmap"},
-                {".pcast", "application/x-podcast"},
-                {".pct", "image/pict"},
-                {".pcx", "application/octet-stream"},
-                {".pcz", "application/octet-stream"},
-                {".pdf", "application/pdf"},
-                {".pfb", "application/octet-stream"},
-                {".pfm", "application/octet-stream"},
-                {".pfx", "application/x-pkcs12"},
-                {".pgm", "image/x-portable-graymap"},
-                {".pic", "image/pict"},
-                {".pict", "image/pict"},
-                {".pkgdef", "text/plain"},
-                {".pkgundef", "text/plain"},
-                {".pko", "application/vnd.ms-pki.pko"},
-                {".pls", "audio/scpls"},
-                {".pma", "application/x-perfmon"},
-                {".pmc", "application/x-perfmon"},
-                {".pml", "application/x-perfmon"},
-                {".pmr", "application/x-perfmon"},
-                {".pmw", "application/x-perfmon"},
-                {".png", "image/png"},
-                {".pnm", "image/x-portable-anymap"},
-                {".pnt", "image/x-macpaint"},
-                {".pntg", "image/x-macpaint"},
-                {".pnz", "image/png"},
-                {".pot", "application/vnd.ms-powerpoint"},
-                {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"},
-                {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"},
-                {".ppa", "application/vnd.ms-powerpoint"},
-                {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"},
-                {".ppm", "image/x-portable-pixmap"},
-                {".pps", "application/vnd.ms-powerpoint"},
-                {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"},
-                {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"},
-                {".ppt", "application/vnd.ms-powerpoint"},
-                {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"},
-                {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
-                {".prf", "application/pics-rules"},
-                {".prm", "application/octet-stream"},
-                {".prx", "application/octet-stream"},
-                {".ps", "application/postscript"},
-                {".psc1", "application/PowerShell"},
-                {".psd", "application/octet-stream"},
-                {".psess", "application/xml"},
-                {".psm", "application/octet-stream"},
-                {".psp", "application/octet-stream"},
-                {".pub", "application/x-mspublisher"},
-                {".pwz", "application/vnd.ms-powerpoint"},
-                {".qht", "text/x-html-insertion"},
-                {".qhtm", "text/x-html-insertion"},
-                {".qt", "video/quicktime"},
-                {".qti", "image/x-quicktime"},
-                {".qtif", "image/x-quicktime"},
-                {".qtl", "application/x-quicktimeplayer"},
-                {".qxd", "application/octet-stream"},
-                {".ra", "audio/x-pn-realaudio"},
-                {".ram", "audio/x-pn-realaudio"},
-                {".rar", "application/octet-stream"},
-                {".ras", "image/x-cmu-raster"},
-                {".rat", "application/rat-file"},
-                {".rc", "text/plain"},
-                {".rc2", "text/plain"},
-                {".rct", "text/plain"},
-                {".rdlc", "application/xml"},
-                {".resx", "application/xml"},
-                {".rf", "image/vnd.rn-realflash"},
-                {".rgb", "image/x-rgb"},
-                {".rgs", "text/plain"},
-                {".rm", "application/vnd.rn-realmedia"},
-                {".rmi", "audio/mid"},
-                {".rmp", "application/vnd.rn-rn_music_package"},
-                {".roff", "application/x-troff"},
-                {".rpm", "audio/x-pn-realaudio-plugin"},
-                {".rqy", "text/x-ms-rqy"},
-                {".rtf", "application/rtf"},
-                {".rtx", "text/richtext"},
-                {".ruleset", "application/xml"},
-                {".s", "text/plain"},
-                {".safariextz", "application/x-safari-safariextz"},
-                {".scd", "application/x-msschedule"},
-                {".sct", "text/scriptlet"},
-                {".sd2", "audio/x-sd2"},
-                {".sdp", "application/sdp"},
-                {".sea", "application/octet-stream"},
-                {".searchConnector-ms", "application/windows-search-connector+xml"},
-                {".setpay", "application/set-payment-initiation"},
-                {".setreg", "application/set-registration-initiation"},
-                {".settings", "application/xml"},
-                {".sgimb", "application/x-sgimb"},
-                {".sgml", "text/sgml"},
-                {".sh", "application/x-sh"},
-                {".shar", "application/x-shar"},
-                {".shtml", "text/html"},
-                {".sit", "application/x-stuffit"},
-                {".sitemap", "application/xml"},
-                {".skin", "application/xml"},
-                {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"},
-                {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"},
-                {".slk", "application/vnd.ms-excel"},
-                {".sln", "text/plain"},
-                {".slupkg-ms", "application/x-ms-license"},
-                {".smd", "audio/x-smd"},
-                {".smi", "application/octet-stream"},
-                {".smx", "audio/x-smd"},
-                {".smz", "audio/x-smd"},
-                {".snd", "audio/basic"},
-                {".snippet", "application/xml"},
-                {".snp", "application/octet-stream"},
-                {".sol", "text/plain"},
-                {".sor", "text/plain"},
-                {".spc", "application/x-pkcs7-certificates"},
-                {".spl", "application/futuresplash"},
-                {".src", "application/x-wais-source"},
-                {".srf", "text/plain"},
-                {".SSISDeploymentManifest", "text/xml"},
-                {".ssm", "application/streamingmedia"},
-                {".sst", "application/vnd.ms-pki.certstore"},
-                {".stl", "application/vnd.ms-pki.stl"},
-                {".sv4cpio", "application/x-sv4cpio"},
-                {".sv4crc", "application/x-sv4crc"},
-                {".svc", "application/xml"},
-                {".swf", "application/x-shockwave-flash"},
-                {".t", "application/x-troff"},
-                {".tar", "application/x-tar"},
-                {".tcl", "application/x-tcl"},
-                {".testrunconfig", "application/xml"},
-                {".testsettings", "application/xml"},
-                {".tex", "application/x-tex"},
-                {".texi", "application/x-texinfo"},
-                {".texinfo", "application/x-texinfo"},
-                {".tgz", "application/x-compressed"},
-                {".thmx", "application/vnd.ms-officetheme"},
-                {".thn", "application/octet-stream"},
-                {".tif", "image/tiff"},
-                {".tiff", "image/tiff"},
-                {".tlh", "text/plain"},
-                {".tli", "text/plain"},
-                {".toc", "application/octet-stream"},
-                {".tr", "application/x-troff"},
-                {".trm", "application/x-msterminal"},
-                {".trx", "application/xml"},
-                {".ts", "video/vnd.dlna.mpeg-tts"},
-                {".tsv", "text/tab-separated-values"},
-                {".ttf", "application/octet-stream"},
-                {".tts", "video/vnd.dlna.mpeg-tts"},
-                {".txt", "text/plain"},
-                {".u32", "application/octet-stream"},
-                {".uls", "text/iuls"},
-                {".user", "text/plain"},
-                {".ustar", "application/x-ustar"},
-                {".vb", "text/plain"},
-                {".vbdproj", "text/plain"},
-                {".vbk", "video/mpeg"},
-                {".vbproj", "text/plain"},
-                {".vbs", "text/vbscript"},
-                {".vcf", "text/x-vcard"},
-                {".vcproj", "Application/xml"},
-                {".vcs", "text/plain"},
-                {".vcxproj", "Application/xml"},
-                {".vddproj", "text/plain"},
-                {".vdp", "text/plain"},
-                {".vdproj", "text/plain"},
-                {".vdx", "application/vnd.ms-visio.viewer"},
-                {".vml", "text/xml"},
-                {".vscontent", "application/xml"},
-                {".vsct", "text/xml"},
-                {".vsd", "application/vnd.visio"},
-                {".vsi", "application/ms-vsi"},
-                {".vsix", "application/vsix"},
-                {".vsixlangpack", "text/xml"},
-                {".vsixmanifest", "text/xml"},
-                {".vsmdi", "application/xml"},
-                {".vspscc", "text/plain"},
-                {".vss", "application/vnd.visio"},
-                {".vsscc", "text/plain"},
-                {".vssettings", "text/xml"},
-                {".vssscc", "text/plain"},
-                {".vst", "application/vnd.visio"},
-                {".vstemplate", "text/xml"},
-                {".vsto", "application/x-ms-vsto"},
-                {".vsw", "application/vnd.visio"},
-                {".vsx", "application/vnd.visio"},
-                {".vtx", "application/vnd.visio"},
-                {".wav", "audio/wav"},
-                {".wave", "audio/wav"},
-                {".wax", "audio/x-ms-wax"},
-                {".wbk", "application/msword"},
-                {".wbmp", "image/vnd.wap.wbmp"},
-                {".wcm", "application/vnd.ms-works"},
-                {".wdb", "application/vnd.ms-works"},
-                {".wdp", "image/vnd.ms-photo"},
-                {".webarchive", "application/x-safari-webarchive"},
-                {".webtest", "application/xml"},
-                {".wiq", "application/xml"},
-                {".wiz", "application/msword"},
-                {".wks", "application/vnd.ms-works"},
-                {".WLMP", "application/wlmoviemaker"},
-                {".wlpginstall", "application/x-wlpg-detect"},
-                {".wlpginstall3", "application/x-wlpg3-detect"},
-                {".wm", "video/x-ms-wm"},
-                {".wma", "audio/x-ms-wma"},
-                {".wmd", "application/x-ms-wmd"},
-                {".wmf", "application/x-msmetafile"},
-                {".wml", "text/vnd.wap.wml"},
-                {".wmlc", "application/vnd.wap.wmlc"},
-                {".wmls", "text/vnd.wap.wmlscript"},
-                {".wmlsc", "application/vnd.wap.wmlscriptc"},
-                {".wmp", "video/x-ms-wmp"},
-                {".wmv", "video/x-ms-wmv"},
-                {".wmx", "video/x-ms-wmx"},
-                {".wmz", "application/x-ms-wmz"},
-                {".wpl", "application/vnd.ms-wpl"},
-                {".wps", "application/vnd.ms-works"},
-                {".wri", "application/x-mswrite"},
-                {".wrl", "x-world/x-vrml"},
-                {".wrz", "x-world/x-vrml"},
-                {".wsc", "text/scriptlet"},
-                {".wsdl", "text/xml"},
-                {".wvx", "video/x-ms-wvx"},
-                {".x", "application/directx"},
-                {".xaf", "x-world/x-vrml"},
-                {".xaml", "application/xaml+xml"},
-                {".xap", "application/x-silverlight-app"},
-                {".xbap", "application/x-ms-xbap"},
-                {".xbm", "image/x-xbitmap"},
-                {".xdr", "text/plain"},
-                {".xht", "application/xhtml+xml"},
-                {".xhtml", "application/xhtml+xml"},
-                {".xla", "application/vnd.ms-excel"},
-                {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"},
-                {".xlc", "application/vnd.ms-excel"},
-                {".xld", "application/vnd.ms-excel"},
-                {".xlk", "application/vnd.ms-excel"},
-                {".xll", "application/vnd.ms-excel"},
-                {".xlm", "application/vnd.ms-excel"},
-                {".xls", "application/vnd.ms-excel"},
-                {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"},
-                {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"},
-                {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
-                {".xlt", "application/vnd.ms-excel"},
-                {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"},
-                {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"},
-                {".xlw", "application/vnd.ms-excel"},
-                {".xml", "text/xml"},
-                {".xmta", "application/xml"},
-                {".xof", "x-world/x-vrml"},
-                {".XOML", "text/plain"},
-                {".xpm", "image/x-xpixmap"},
-                {".xps", "application/vnd.ms-xpsdocument"},
-                {".xrm-ms", "text/xml"},
-                {".xsc", "application/xml"},
-                {".xsd", "text/xml"},
-                {".xsf", "text/xml"},
-                {".xsl", "text/xml"},
-                {".xslt", "text/xml"},
-                {".xsn", "application/octet-stream"},
-                {".xss", "application/xml"},
-                {".xtp", "application/octet-stream"},
-                {".xwd", "image/x-xwindowdump"},
-                {".z", "application/x-compress"},
-                {".zip", "application/x-zip-compressed"}
-
-                #endregion
-            };
-
-        /// 
-        ///     Get mime type from file extension
-        /// 
-        /// Extension with or without dot as prefix.
-        /// mime type
-        public static string GetMimeType(string extension)
-        {
-            if (extension == null)
-            {
-                throw new ArgumentNullException("extension");
-            }
-
-            if (!extension.StartsWith("."))
-            {
-                extension = "." + extension;
-            }
-
-            string mime;
-
-            return Mappings.TryGetValue(extension, out mime) ? mime : "application/octet-stream";
-        }
-    }
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.Infrastructure.Net
+{
+    /// 
+    ///     Mappings between file extensions and mime types
+    /// 
+    public static class MimeMapping
+    {
+        private static readonly IDictionary Mappings =
+            new Dictionary(StringComparer.InvariantCultureIgnoreCase)
+            {
+                #region Big freaking list of mime types
+
+                // combination of values from Windows 7 Registry and 
+                // from C:\Windows\System32\inetsrv\config\applicationHost.config
+                // some added, including .7z and .dat
+                {".323", "text/h323"},
+                {".3g2", "video/3gpp2"},
+                {".3gp", "video/3gpp"},
+                {".3gp2", "video/3gpp2"},
+                {".3gpp", "video/3gpp"},
+                {".7z", "application/x-7z-compressed"},
+                {".aa", "audio/audible"},
+                {".AAC", "audio/aac"},
+                {".aaf", "application/octet-stream"},
+                {".aax", "audio/vnd.audible.aax"},
+                {".ac3", "audio/ac3"},
+                {".aca", "application/octet-stream"},
+                {".accda", "application/msaccess.addin"},
+                {".accdb", "application/msaccess"},
+                {".accdc", "application/msaccess.cab"},
+                {".accde", "application/msaccess"},
+                {".accdr", "application/msaccess.runtime"},
+                {".accdt", "application/msaccess"},
+                {".accdw", "application/msaccess.webapplication"},
+                {".accft", "application/msaccess.ftemplate"},
+                {".acx", "application/internet-property-stream"},
+                {".AddIn", "text/xml"},
+                {".ade", "application/msaccess"},
+                {".adobebridge", "application/x-bridge-url"},
+                {".adp", "application/msaccess"},
+                {".ADT", "audio/vnd.dlna.adts"},
+                {".ADTS", "audio/aac"},
+                {".afm", "application/octet-stream"},
+                {".ai", "application/postscript"},
+                {".aif", "audio/x-aiff"},
+                {".aifc", "audio/aiff"},
+                {".aiff", "audio/aiff"},
+                {".air", "application/vnd.adobe.air-application-installer-package+zip"},
+                {".amc", "application/x-mpeg"},
+                {".application", "application/x-ms-application"},
+                {".art", "image/x-jg"},
+                {".asa", "application/xml"},
+                {".asax", "application/xml"},
+                {".ascx", "application/xml"},
+                {".asd", "application/octet-stream"},
+                {".asf", "video/x-ms-asf"},
+                {".ashx", "application/xml"},
+                {".asi", "application/octet-stream"},
+                {".asm", "text/plain"},
+                {".asmx", "application/xml"},
+                {".aspx", "application/xml"},
+                {".asr", "video/x-ms-asf"},
+                {".asx", "video/x-ms-asf"},
+                {".atom", "application/atom+xml"},
+                {".au", "audio/basic"},
+                {".avi", "video/x-msvideo"},
+                {".axs", "application/olescript"},
+                {".bas", "text/plain"},
+                {".bcpio", "application/x-bcpio"},
+                {".bin", "application/octet-stream"},
+                {".bmp", "image/bmp"},
+                {".c", "text/plain"},
+                {".cab", "application/octet-stream"},
+                {".caf", "audio/x-caf"},
+                {".calx", "application/vnd.ms-office.calx"},
+                {".cat", "application/vnd.ms-pki.seccat"},
+                {".cc", "text/plain"},
+                {".cd", "text/plain"},
+                {".cdda", "audio/aiff"},
+                {".cdf", "application/x-cdf"},
+                {".cer", "application/x-x509-ca-cert"},
+                {".chm", "application/octet-stream"},
+                {".class", "application/x-java-applet"},
+                {".clp", "application/x-msclip"},
+                {".cmx", "image/x-cmx"},
+                {".cnf", "text/plain"},
+                {".cod", "image/cis-cod"},
+                {".config", "application/xml"},
+                {".contact", "text/x-ms-contact"},
+                {".coverage", "application/xml"},
+                {".cpio", "application/x-cpio"},
+                {".cpp", "text/plain"},
+                {".crd", "application/x-mscardfile"},
+                {".crl", "application/pkix-crl"},
+                {".crt", "application/x-x509-ca-cert"},
+                {".cs", "text/plain"},
+                {".csdproj", "text/plain"},
+                {".csh", "application/x-csh"},
+                {".csproj", "text/plain"},
+                {".css", "text/css"},
+                {".csv", "text/csv"},
+                {".cur", "application/octet-stream"},
+                {".cxx", "text/plain"},
+                {".dat", "application/octet-stream"},
+                {".datasource", "application/xml"},
+                {".dbproj", "text/plain"},
+                {".dcr", "application/x-director"},
+                {".def", "text/plain"},
+                {".deploy", "application/octet-stream"},
+                {".der", "application/x-x509-ca-cert"},
+                {".dgml", "application/xml"},
+                {".dib", "image/bmp"},
+                {".dif", "video/x-dv"},
+                {".dir", "application/x-director"},
+                {".disco", "text/xml"},
+                {".dll", "application/x-msdownload"},
+                {".dll.config", "text/xml"},
+                {".dlm", "text/dlm"},
+                {".doc", "application/msword"},
+                {".docm", "application/vnd.ms-word.document.macroEnabled.12"},
+                {".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"},
+                {".dot", "application/msword"},
+                {".dotm", "application/vnd.ms-word.template.macroEnabled.12"},
+                {".dotx", "application/vnd.openxmlformats-officedocument.wordprocessingml.template"},
+                {".dsp", "application/octet-stream"},
+                {".dsw", "text/plain"},
+                {".dtd", "text/xml"},
+                {".dtsConfig", "text/xml"},
+                {".dv", "video/x-dv"},
+                {".dvi", "application/x-dvi"},
+                {".dwf", "drawing/x-dwf"},
+                {".dwp", "application/octet-stream"},
+                {".dxr", "application/x-director"},
+                {".eml", "message/rfc822"},
+                {".emz", "application/octet-stream"},
+                {".eot", "application/octet-stream"},
+                {".eps", "application/postscript"},
+                {".etl", "application/etl"},
+                {".etx", "text/x-setext"},
+                {".evy", "application/envoy"},
+                {".exe", "application/octet-stream"},
+                {".exe.config", "text/xml"},
+                {".fdf", "application/vnd.fdf"},
+                {".fif", "application/fractals"},
+                {".filters", "Application/xml"},
+                {".fla", "application/octet-stream"},
+                {".flr", "x-world/x-vrml"},
+                {".flv", "video/x-flv"},
+                {".fsscript", "application/fsharp-script"},
+                {".fsx", "application/fsharp-script"},
+                {".generictest", "application/xml"},
+                {".gif", "image/gif"},
+                {".group", "text/x-ms-group"},
+                {".gsm", "audio/x-gsm"},
+                {".gtar", "application/x-gtar"},
+                {".gz", "application/x-gzip"},
+                {".h", "text/plain"},
+                {".hdf", "application/x-hdf"},
+                {".hdml", "text/x-hdml"},
+                {".hhc", "application/x-oleobject"},
+                {".hhk", "application/octet-stream"},
+                {".hhp", "application/octet-stream"},
+                {".hlp", "application/winhlp"},
+                {".hpp", "text/plain"},
+                {".hqx", "application/mac-binhex40"},
+                {".hta", "application/hta"},
+                {".htc", "text/x-component"},
+                {".htm", "text/html"},
+                {".html", "text/html"},
+                {".htt", "text/webviewhtml"},
+                {".hxa", "application/xml"},
+                {".hxc", "application/xml"},
+                {".hxd", "application/octet-stream"},
+                {".hxe", "application/xml"},
+                {".hxf", "application/xml"},
+                {".hxh", "application/octet-stream"},
+                {".hxi", "application/octet-stream"},
+                {".hxk", "application/xml"},
+                {".hxq", "application/octet-stream"},
+                {".hxr", "application/octet-stream"},
+                {".hxs", "application/octet-stream"},
+                {".hxt", "text/html"},
+                {".hxv", "application/xml"},
+                {".hxw", "application/octet-stream"},
+                {".hxx", "text/plain"},
+                {".i", "text/plain"},
+                {".ico", "image/x-icon"},
+                {".ics", "application/octet-stream"},
+                {".idl", "text/plain"},
+                {".ief", "image/ief"},
+                {".iii", "application/x-iphone"},
+                {".inc", "text/plain"},
+                {".inf", "application/octet-stream"},
+                {".inl", "text/plain"},
+                {".ins", "application/x-internet-signup"},
+                {".ipa", "application/x-itunes-ipa"},
+                {".ipg", "application/x-itunes-ipg"},
+                {".ipproj", "text/plain"},
+                {".ipsw", "application/x-itunes-ipsw"},
+                {".iqy", "text/x-ms-iqy"},
+                {".isp", "application/x-internet-signup"},
+                {".ite", "application/x-itunes-ite"},
+                {".itlp", "application/x-itunes-itlp"},
+                {".itms", "application/x-itunes-itms"},
+                {".itpc", "application/x-itunes-itpc"},
+                {".IVF", "video/x-ivf"},
+                {".jar", "application/java-archive"},
+                {".java", "application/octet-stream"},
+                {".jck", "application/liquidmotion"},
+                {".jcz", "application/liquidmotion"},
+                {".jfif", "image/pjpeg"},
+                {".jnlp", "application/x-java-jnlp-file"},
+                {".jpb", "application/octet-stream"},
+                {".jpe", "image/jpeg"},
+                {".jpeg", "image/jpeg"},
+                {".jpg", "image/jpeg"},
+                {".js", "application/x-javascript"},
+                {".jsx", "text/jscript"},
+                {".jsxbin", "text/plain"},
+                {".latex", "application/x-latex"},
+                {".library-ms", "application/windows-library+xml"},
+                {".lit", "application/x-ms-reader"},
+                {".loadtest", "application/xml"},
+                {".lpk", "application/octet-stream"},
+                {".lsf", "video/x-la-asf"},
+                {".lst", "text/plain"},
+                {".lsx", "video/x-la-asf"},
+                {".lzh", "application/octet-stream"},
+                {".m13", "application/x-msmediaview"},
+                {".m14", "application/x-msmediaview"},
+                {".m1v", "video/mpeg"},
+                {".m2t", "video/vnd.dlna.mpeg-tts"},
+                {".m2ts", "video/vnd.dlna.mpeg-tts"},
+                {".m2v", "video/mpeg"},
+                {".m3u", "audio/x-mpegurl"},
+                {".m3u8", "audio/x-mpegurl"},
+                {".m4a", "audio/m4a"},
+                {".m4b", "audio/m4b"},
+                {".m4p", "audio/m4p"},
+                {".m4r", "audio/x-m4r"},
+                {".m4v", "video/x-m4v"},
+                {".mac", "image/x-macpaint"},
+                {".mak", "text/plain"},
+                {".man", "application/x-troff-man"},
+                {".manifest", "application/x-ms-manifest"},
+                {".map", "text/plain"},
+                {".master", "application/xml"},
+                {".mda", "application/msaccess"},
+                {".mdb", "application/x-msaccess"},
+                {".mde", "application/msaccess"},
+                {".mdp", "application/octet-stream"},
+                {".me", "application/x-troff-me"},
+                {".mfp", "application/x-shockwave-flash"},
+                {".mht", "message/rfc822"},
+                {".mhtml", "message/rfc822"},
+                {".mid", "audio/mid"},
+                {".midi", "audio/mid"},
+                {".mix", "application/octet-stream"},
+                {".mk", "text/plain"},
+                {".mmf", "application/x-smaf"},
+                {".mno", "text/xml"},
+                {".mny", "application/x-msmoney"},
+                {".mod", "video/mpeg"},
+                {".mov", "video/quicktime"},
+                {".movie", "video/x-sgi-movie"},
+                {".mp2", "video/mpeg"},
+                {".mp2v", "video/mpeg"},
+                {".mp3", "audio/mpeg"},
+                {".mp4", "video/mp4"},
+                {".mp4v", "video/mp4"},
+                {".mpa", "video/mpeg"},
+                {".mpe", "video/mpeg"},
+                {".mpeg", "video/mpeg"},
+                {".mpf", "application/vnd.ms-mediapackage"},
+                {".mpg", "video/mpeg"},
+                {".mpp", "application/vnd.ms-project"},
+                {".mpv2", "video/mpeg"},
+                {".mqv", "video/quicktime"},
+                {".ms", "application/x-troff-ms"},
+                {".msi", "application/octet-stream"},
+                {".mso", "application/octet-stream"},
+                {".mts", "video/vnd.dlna.mpeg-tts"},
+                {".mtx", "application/xml"},
+                {".mvb", "application/x-msmediaview"},
+                {".mvc", "application/x-miva-compiled"},
+                {".mxp", "application/x-mmxp"},
+                {".nc", "application/x-netcdf"},
+                {".nsc", "video/x-ms-asf"},
+                {".nws", "message/rfc822"},
+                {".ocx", "application/octet-stream"},
+                {".oda", "application/oda"},
+                {".odc", "text/x-ms-odc"},
+                {".odh", "text/plain"},
+                {".odl", "text/plain"},
+                {".odp", "application/vnd.oasis.opendocument.presentation"},
+                {".ods", "application/oleobject"},
+                {".odt", "application/vnd.oasis.opendocument.text"},
+                {".one", "application/onenote"},
+                {".onea", "application/onenote"},
+                {".onepkg", "application/onenote"},
+                {".onetmp", "application/onenote"},
+                {".onetoc", "application/onenote"},
+                {".onetoc2", "application/onenote"},
+                {".orderedtest", "application/xml"},
+                {".osdx", "application/opensearchdescription+xml"},
+                {".p10", "application/pkcs10"},
+                {".p12", "application/x-pkcs12"},
+                {".p7b", "application/x-pkcs7-certificates"},
+                {".p7c", "application/pkcs7-mime"},
+                {".p7m", "application/pkcs7-mime"},
+                {".p7r", "application/x-pkcs7-certreqresp"},
+                {".p7s", "application/pkcs7-signature"},
+                {".pbm", "image/x-portable-bitmap"},
+                {".pcast", "application/x-podcast"},
+                {".pct", "image/pict"},
+                {".pcx", "application/octet-stream"},
+                {".pcz", "application/octet-stream"},
+                {".pdf", "application/pdf"},
+                {".pfb", "application/octet-stream"},
+                {".pfm", "application/octet-stream"},
+                {".pfx", "application/x-pkcs12"},
+                {".pgm", "image/x-portable-graymap"},
+                {".pic", "image/pict"},
+                {".pict", "image/pict"},
+                {".pkgdef", "text/plain"},
+                {".pkgundef", "text/plain"},
+                {".pko", "application/vnd.ms-pki.pko"},
+                {".pls", "audio/scpls"},
+                {".pma", "application/x-perfmon"},
+                {".pmc", "application/x-perfmon"},
+                {".pml", "application/x-perfmon"},
+                {".pmr", "application/x-perfmon"},
+                {".pmw", "application/x-perfmon"},
+                {".png", "image/png"},
+                {".pnm", "image/x-portable-anymap"},
+                {".pnt", "image/x-macpaint"},
+                {".pntg", "image/x-macpaint"},
+                {".pnz", "image/png"},
+                {".pot", "application/vnd.ms-powerpoint"},
+                {".potm", "application/vnd.ms-powerpoint.template.macroEnabled.12"},
+                {".potx", "application/vnd.openxmlformats-officedocument.presentationml.template"},
+                {".ppa", "application/vnd.ms-powerpoint"},
+                {".ppam", "application/vnd.ms-powerpoint.addin.macroEnabled.12"},
+                {".ppm", "image/x-portable-pixmap"},
+                {".pps", "application/vnd.ms-powerpoint"},
+                {".ppsm", "application/vnd.ms-powerpoint.slideshow.macroEnabled.12"},
+                {".ppsx", "application/vnd.openxmlformats-officedocument.presentationml.slideshow"},
+                {".ppt", "application/vnd.ms-powerpoint"},
+                {".pptm", "application/vnd.ms-powerpoint.presentation.macroEnabled.12"},
+                {".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation"},
+                {".prf", "application/pics-rules"},
+                {".prm", "application/octet-stream"},
+                {".prx", "application/octet-stream"},
+                {".ps", "application/postscript"},
+                {".psc1", "application/PowerShell"},
+                {".psd", "application/octet-stream"},
+                {".psess", "application/xml"},
+                {".psm", "application/octet-stream"},
+                {".psp", "application/octet-stream"},
+                {".pub", "application/x-mspublisher"},
+                {".pwz", "application/vnd.ms-powerpoint"},
+                {".qht", "text/x-html-insertion"},
+                {".qhtm", "text/x-html-insertion"},
+                {".qt", "video/quicktime"},
+                {".qti", "image/x-quicktime"},
+                {".qtif", "image/x-quicktime"},
+                {".qtl", "application/x-quicktimeplayer"},
+                {".qxd", "application/octet-stream"},
+                {".ra", "audio/x-pn-realaudio"},
+                {".ram", "audio/x-pn-realaudio"},
+                {".rar", "application/octet-stream"},
+                {".ras", "image/x-cmu-raster"},
+                {".rat", "application/rat-file"},
+                {".rc", "text/plain"},
+                {".rc2", "text/plain"},
+                {".rct", "text/plain"},
+                {".rdlc", "application/xml"},
+                {".resx", "application/xml"},
+                {".rf", "image/vnd.rn-realflash"},
+                {".rgb", "image/x-rgb"},
+                {".rgs", "text/plain"},
+                {".rm", "application/vnd.rn-realmedia"},
+                {".rmi", "audio/mid"},
+                {".rmp", "application/vnd.rn-rn_music_package"},
+                {".roff", "application/x-troff"},
+                {".rpm", "audio/x-pn-realaudio-plugin"},
+                {".rqy", "text/x-ms-rqy"},
+                {".rtf", "application/rtf"},
+                {".rtx", "text/richtext"},
+                {".ruleset", "application/xml"},
+                {".s", "text/plain"},
+                {".safariextz", "application/x-safari-safariextz"},
+                {".scd", "application/x-msschedule"},
+                {".sct", "text/scriptlet"},
+                {".sd2", "audio/x-sd2"},
+                {".sdp", "application/sdp"},
+                {".sea", "application/octet-stream"},
+                {".searchConnector-ms", "application/windows-search-connector+xml"},
+                {".setpay", "application/set-payment-initiation"},
+                {".setreg", "application/set-registration-initiation"},
+                {".settings", "application/xml"},
+                {".sgimb", "application/x-sgimb"},
+                {".sgml", "text/sgml"},
+                {".sh", "application/x-sh"},
+                {".shar", "application/x-shar"},
+                {".shtml", "text/html"},
+                {".sit", "application/x-stuffit"},
+                {".sitemap", "application/xml"},
+                {".skin", "application/xml"},
+                {".sldm", "application/vnd.ms-powerpoint.slide.macroEnabled.12"},
+                {".sldx", "application/vnd.openxmlformats-officedocument.presentationml.slide"},
+                {".slk", "application/vnd.ms-excel"},
+                {".sln", "text/plain"},
+                {".slupkg-ms", "application/x-ms-license"},
+                {".smd", "audio/x-smd"},
+                {".smi", "application/octet-stream"},
+                {".smx", "audio/x-smd"},
+                {".smz", "audio/x-smd"},
+                {".snd", "audio/basic"},
+                {".snippet", "application/xml"},
+                {".snp", "application/octet-stream"},
+                {".sol", "text/plain"},
+                {".sor", "text/plain"},
+                {".spc", "application/x-pkcs7-certificates"},
+                {".spl", "application/futuresplash"},
+                {".src", "application/x-wais-source"},
+                {".srf", "text/plain"},
+                {".SSISDeploymentManifest", "text/xml"},
+                {".ssm", "application/streamingmedia"},
+                {".sst", "application/vnd.ms-pki.certstore"},
+                {".stl", "application/vnd.ms-pki.stl"},
+                {".sv4cpio", "application/x-sv4cpio"},
+                {".sv4crc", "application/x-sv4crc"},
+                {".svc", "application/xml"},
+                {".swf", "application/x-shockwave-flash"},
+                {".t", "application/x-troff"},
+                {".tar", "application/x-tar"},
+                {".tcl", "application/x-tcl"},
+                {".testrunconfig", "application/xml"},
+                {".testsettings", "application/xml"},
+                {".tex", "application/x-tex"},
+                {".texi", "application/x-texinfo"},
+                {".texinfo", "application/x-texinfo"},
+                {".tgz", "application/x-compressed"},
+                {".thmx", "application/vnd.ms-officetheme"},
+                {".thn", "application/octet-stream"},
+                {".tif", "image/tiff"},
+                {".tiff", "image/tiff"},
+                {".tlh", "text/plain"},
+                {".tli", "text/plain"},
+                {".toc", "application/octet-stream"},
+                {".tr", "application/x-troff"},
+                {".trm", "application/x-msterminal"},
+                {".trx", "application/xml"},
+                {".ts", "video/vnd.dlna.mpeg-tts"},
+                {".tsv", "text/tab-separated-values"},
+                {".ttf", "application/octet-stream"},
+                {".tts", "video/vnd.dlna.mpeg-tts"},
+                {".txt", "text/plain"},
+                {".u32", "application/octet-stream"},
+                {".uls", "text/iuls"},
+                {".user", "text/plain"},
+                {".ustar", "application/x-ustar"},
+                {".vb", "text/plain"},
+                {".vbdproj", "text/plain"},
+                {".vbk", "video/mpeg"},
+                {".vbproj", "text/plain"},
+                {".vbs", "text/vbscript"},
+                {".vcf", "text/x-vcard"},
+                {".vcproj", "Application/xml"},
+                {".vcs", "text/plain"},
+                {".vcxproj", "Application/xml"},
+                {".vddproj", "text/plain"},
+                {".vdp", "text/plain"},
+                {".vdproj", "text/plain"},
+                {".vdx", "application/vnd.ms-visio.viewer"},
+                {".vml", "text/xml"},
+                {".vscontent", "application/xml"},
+                {".vsct", "text/xml"},
+                {".vsd", "application/vnd.visio"},
+                {".vsi", "application/ms-vsi"},
+                {".vsix", "application/vsix"},
+                {".vsixlangpack", "text/xml"},
+                {".vsixmanifest", "text/xml"},
+                {".vsmdi", "application/xml"},
+                {".vspscc", "text/plain"},
+                {".vss", "application/vnd.visio"},
+                {".vsscc", "text/plain"},
+                {".vssettings", "text/xml"},
+                {".vssscc", "text/plain"},
+                {".vst", "application/vnd.visio"},
+                {".vstemplate", "text/xml"},
+                {".vsto", "application/x-ms-vsto"},
+                {".vsw", "application/vnd.visio"},
+                {".vsx", "application/vnd.visio"},
+                {".vtx", "application/vnd.visio"},
+                {".wav", "audio/wav"},
+                {".wave", "audio/wav"},
+                {".wax", "audio/x-ms-wax"},
+                {".wbk", "application/msword"},
+                {".wbmp", "image/vnd.wap.wbmp"},
+                {".wcm", "application/vnd.ms-works"},
+                {".wdb", "application/vnd.ms-works"},
+                {".wdp", "image/vnd.ms-photo"},
+                {".webarchive", "application/x-safari-webarchive"},
+                {".webtest", "application/xml"},
+                {".wiq", "application/xml"},
+                {".wiz", "application/msword"},
+                {".wks", "application/vnd.ms-works"},
+                {".WLMP", "application/wlmoviemaker"},
+                {".wlpginstall", "application/x-wlpg-detect"},
+                {".wlpginstall3", "application/x-wlpg3-detect"},
+                {".wm", "video/x-ms-wm"},
+                {".wma", "audio/x-ms-wma"},
+                {".wmd", "application/x-ms-wmd"},
+                {".wmf", "application/x-msmetafile"},
+                {".wml", "text/vnd.wap.wml"},
+                {".wmlc", "application/vnd.wap.wmlc"},
+                {".wmls", "text/vnd.wap.wmlscript"},
+                {".wmlsc", "application/vnd.wap.wmlscriptc"},
+                {".wmp", "video/x-ms-wmp"},
+                {".wmv", "video/x-ms-wmv"},
+                {".wmx", "video/x-ms-wmx"},
+                {".wmz", "application/x-ms-wmz"},
+                {".wpl", "application/vnd.ms-wpl"},
+                {".wps", "application/vnd.ms-works"},
+                {".wri", "application/x-mswrite"},
+                {".wrl", "x-world/x-vrml"},
+                {".wrz", "x-world/x-vrml"},
+                {".wsc", "text/scriptlet"},
+                {".wsdl", "text/xml"},
+                {".wvx", "video/x-ms-wvx"},
+                {".x", "application/directx"},
+                {".xaf", "x-world/x-vrml"},
+                {".xaml", "application/xaml+xml"},
+                {".xap", "application/x-silverlight-app"},
+                {".xbap", "application/x-ms-xbap"},
+                {".xbm", "image/x-xbitmap"},
+                {".xdr", "text/plain"},
+                {".xht", "application/xhtml+xml"},
+                {".xhtml", "application/xhtml+xml"},
+                {".xla", "application/vnd.ms-excel"},
+                {".xlam", "application/vnd.ms-excel.addin.macroEnabled.12"},
+                {".xlc", "application/vnd.ms-excel"},
+                {".xld", "application/vnd.ms-excel"},
+                {".xlk", "application/vnd.ms-excel"},
+                {".xll", "application/vnd.ms-excel"},
+                {".xlm", "application/vnd.ms-excel"},
+                {".xls", "application/vnd.ms-excel"},
+                {".xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12"},
+                {".xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12"},
+                {".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"},
+                {".xlt", "application/vnd.ms-excel"},
+                {".xltm", "application/vnd.ms-excel.template.macroEnabled.12"},
+                {".xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template"},
+                {".xlw", "application/vnd.ms-excel"},
+                {".xml", "text/xml"},
+                {".xmta", "application/xml"},
+                {".xof", "x-world/x-vrml"},
+                {".XOML", "text/plain"},
+                {".xpm", "image/x-xpixmap"},
+                {".xps", "application/vnd.ms-xpsdocument"},
+                {".xrm-ms", "text/xml"},
+                {".xsc", "application/xml"},
+                {".xsd", "text/xml"},
+                {".xsf", "text/xml"},
+                {".xsl", "text/xml"},
+                {".xslt", "text/xml"},
+                {".xsn", "application/octet-stream"},
+                {".xss", "application/xml"},
+                {".xtp", "application/octet-stream"},
+                {".xwd", "image/x-xwindowdump"},
+                {".z", "application/x-compress"},
+                {".zip", "application/x-zip-compressed"}
+
+                #endregion
+            };
+
+        /// 
+        ///     Get mime type from file extension
+        /// 
+        /// Extension with or without dot as prefix.
+        /// mime type
+        public static string GetMimeType(string extension)
+        {
+            if (extension == null)
+            {
+                throw new ArgumentNullException("extension");
+            }
+
+            if (!extension.StartsWith("."))
+            {
+                extension = "." + extension;
+            }
+
+            string mime;
+
+            return Mappings.TryGetValue(extension, out mime) ? mime : "application/octet-stream";
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Plugins/Configuration.cs b/src/Server/Coderr.Server.Infrastructure/Plugins/Configuration.cs
new file mode 100644
index 00000000..fd12129d
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Plugins/Configuration.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Reflection;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Coderr.Server.Infrastructure.Plugins
+{
+    /// 
+    ///     Configuration possibilities for plugins
+    /// 
+    public abstract class Configuration
+    {
+        protected Configuration()
+        {
+            Menu = new MenuConfiguration();
+        }
+
+        /// 
+        ///     Add items to the different menus.
+        /// 
+        public MenuConfiguration Menu { get; }
+
+        /// 
+        /// Register multiple services in the IoC container.
+        /// 
+        /// Configuration context
+        public abstract void ConfigureServices(IServiceCollection services);
+
+        /// 
+        ///     Register a service
+        /// 
+        /// Type of service to register
+        public abstract void RegisterInstance(TService service);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Plugins/IPlugin.cs b/src/Server/Coderr.Server.Infrastructure/Plugins/IPlugin.cs
new file mode 100644
index 00000000..ffc0c2e5
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Plugins/IPlugin.cs
@@ -0,0 +1,19 @@
+namespace Coderr.Server.Infrastructure.Plugins
+{
+    /// 
+    ///     Represents a plugin in Coderr.
+    /// 
+    public interface IPlugin
+    {
+        /// 
+        ///     Done after the boot to collect application configuration.
+        /// 
+        /// configuration
+        void Configure(Configuration config);
+
+        /// 
+        ///     The application is starting, nothing is yet configured.
+        /// 
+        void Preload();
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Plugins/MenuConfiguration.cs b/src/Server/Coderr.Server.Infrastructure/Plugins/MenuConfiguration.cs
new file mode 100644
index 00000000..f948bc7e
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Plugins/MenuConfiguration.cs
@@ -0,0 +1,40 @@
+namespace Coderr.Server.Infrastructure.Plugins
+{
+    /// 
+    ///     Main menu
+    /// 
+    public class MenuConfiguration
+    {
+        private readonly MenuItem _menu = new MenuItem("GlobalOverview", "Overview", "#/");
+
+        public MenuConfiguration()
+        {
+            _menu.Add("Application", "Application", "#/application/:applicationId/");
+            _menu.Add("Incident", "Incident", "#/application/:applicationId/incident/:incidentId/");
+            _menu.Add("System", "System", "#/system");
+        }
+
+        /// 
+        ///     Add to system menu (where the 'settings' and 'admin panel' menu items are visible)
+        /// 
+        /// 
+        ///     menu identifier (assigned as HTML Element id), so try to use a somewhat unique name, or we'll get a
+        ///     very sad face when the menu stop working.
+        /// 
+        /// Menu item title
+        /// Route in SPA. Do note that the same route will be used when requesting information for the page.
+        public void AddToSystemMenu(string name, string title, string spaRoute)
+        {
+            _menu["System"].Add(name, title, spaRoute);
+        }
+
+        /// 
+        ///     Generate menu
+        /// 
+        /// 
+        public MenuItem BuildMenu()
+        {
+            return _menu;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Plugins/MenuItem.cs b/src/Server/Coderr.Server.Infrastructure/Plugins/MenuItem.cs
new file mode 100644
index 00000000..6f5639bd
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Plugins/MenuItem.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.Infrastructure.Plugins
+{
+    /// 
+    ///     Item for .
+    /// 
+    public class MenuItem
+    {
+        private readonly IDictionary _items = new Dictionary();
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// 
+        ///     menu identifier (assigned as HTML Element id), so try to use a somewhat unique name, or we'll get a
+        ///     very sad face when the menu stop working.
+        /// 
+        /// Menu item title
+        /// Page to visit
+        public MenuItem(string name, string title, string spaRoute)
+        {
+            Title = title ?? throw new ArgumentNullException(nameof(title));
+            SpaRoute = spaRoute ?? throw new ArgumentNullException(nameof(spaRoute));
+            Name = name;
+        }
+
+        /// 
+        ///     Child items
+        /// 
+        /// Name in pascal case
+        /// item
+        /// name
+        public MenuItem this[string name] => _items[name];
+
+        /// 
+        ///     All children
+        /// 
+        public IEnumerable Items => _items.Values;
+
+        /// 
+        ///     Name in PascalCase.
+        /// 
+        public string Name { get; }
+
+        /// 
+        ///     Hashbang path
+        /// 
+        public string SpaRoute { get; }
+
+        /// 
+        ///     Title (menu text)
+        /// 
+        public string Title { get; }
+
+
+        /// 
+        ///     Add child item
+        /// 
+        /// 
+        /// 
+        /// 
+        public void Add(string name, string title, string link)
+        {
+            if (title == null) throw new ArgumentNullException(nameof(title));
+            if (link == null) throw new ArgumentNullException(nameof(link));
+            if (name.Contains(" "))
+                throw new FormatException("Name may not contain spaces or any other stupid characters.");
+
+            _items.Add(name, new MenuItem(name, title, link));
+        }
+
+        /// 
+        ///     Change path to absolute path (except for hash routes).
+        /// 
+        /// 
+        /// 
+        public string ToAbsolute(Func formatter)
+        {
+            if (!SpaRoute.StartsWith("#"))
+                return formatter(SpaRoute);
+
+            return SpaRoute;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/Security/AuthenticationTypes.cs b/src/Server/Coderr.Server.Infrastructure/Security/AuthenticationTypes.cs
new file mode 100644
index 00000000..5314c3fa
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/Security/AuthenticationTypes.cs
@@ -0,0 +1,7 @@
+namespace Coderr.Server.Infrastructure.Security
+{
+    public static class AuthenticationTypes
+    {
+        public const string Default = "ApplicationCookie";
+    }
+}
diff --git a/src/Server/OneTrueError.Data.Common/Security/UpdatesLoggedInAccountAttribute.cs b/src/Server/Coderr.Server.Infrastructure/Security/UpdatesLoggedInAccountAttribute.cs
similarity index 86%
rename from src/Server/OneTrueError.Data.Common/Security/UpdatesLoggedInAccountAttribute.cs
rename to src/Server/Coderr.Server.Infrastructure/Security/UpdatesLoggedInAccountAttribute.cs
index d6950f42..3f7a09b4 100644
--- a/src/Server/OneTrueError.Data.Common/Security/UpdatesLoggedInAccountAttribute.cs
+++ b/src/Server/Coderr.Server.Infrastructure/Security/UpdatesLoggedInAccountAttribute.cs
@@ -1,6 +1,6 @@
 using System;
 
-namespace OneTrueError.Infrastructure.Security
+namespace Coderr.Server.Infrastructure.Security
 {
     /// 
     ///     The handler that this attribute is placed on will update the currently logged in user, so the host need to update
diff --git a/src/Server/Coderr.Server.Infrastructure/SetupTools.cs b/src/Server/Coderr.Server.Infrastructure/SetupTools.cs
new file mode 100644
index 00000000..36830ba9
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/SetupTools.cs
@@ -0,0 +1,8 @@
+namespace Coderr.Server.Infrastructure
+{
+    public static class SetupTools
+    {
+        public static ISetupDatabaseTools DbTools { get; set; }
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Infrastructure/TypeHelper.cs b/src/Server/Coderr.Server.Infrastructure/TypeHelper.cs
new file mode 100644
index 00000000..10384141
--- /dev/null
+++ b/src/Server/Coderr.Server.Infrastructure/TypeHelper.cs
@@ -0,0 +1,95 @@
+using System;
+using System.Linq;
+using System.Reflection;
+
+namespace Coderr.Server.Infrastructure
+{
+    /// 
+    ///     Reflection helper.
+    /// 
+    public class TypeHelper
+    {
+        /// 
+        ///     Create an instance of an assembly type
+        /// 
+        /// Type name, assembly name
+        /// created instance
+        /// 
+        ///     
+        ///         Can be used when the type name is not a fully qualified assembly name.
+        ///     
+        /// 
+        public static object CreateAssemblyObject(string typeName)
+        {
+            var parts = typeName.Split(',').Select(x => x.Trim()).ToArray();
+            var assemblyName = parts[1];
+            var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name.Equals(assemblyName));
+            if (asm == null)
+                throw new InvalidOperationException("Failed to find assembly '" + assemblyName + "'.");
+            var type = asm.GetType(parts[0]);
+            if (type == null)
+                throw new InvalidOperationException("Failed to find type '" + parts[0] + "' in assembly '" +
+                                                    assemblyName + "'.");
+            return Activator.CreateInstance(type);
+        }
+
+
+        /// 
+        ///     Create an instance of an assembly type
+        /// 
+        /// Type name, assembly name
+        /// created instance
+        /// 
+        ///     
+        ///         Can be used when the type name is not a fully qualified assembly name.
+        ///     
+        /// 
+        public static Type CreateAssemblyType(string typeName)
+        {
+            var parts = typeName.Split(',').Select(x => x.Trim()).ToArray();
+            var assemblyName = parts[1];
+            var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name.Equals(assemblyName));
+            if (asm == null)
+                throw new InvalidOperationException("Failed to find assembly '" + assemblyName + "'.");
+            var type = asm.GetType(parts[0]);
+            if (type == null)
+                throw new InvalidOperationException("Failed to find type '" + parts[0] + "' in assembly '" +
+                                                    assemblyName + "'.");
+            return type;
+        }
+
+        /// 
+        ///     Create an instance of an assembly type
+        /// 
+        /// Type name, assembly name
+        /// 
+        /// created instance
+        /// 
+        ///     
+        ///         Can be used when the type name is not a fully qualified assembly name.
+        ///     
+        /// 
+        public static object CreateAssemblyObject(string typeName, params object[] constructorArguments)
+        {
+            var parts = typeName.Split(',').Select(x => x.Trim()).ToArray();
+            var assemblyName = parts[1];
+            var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name.Equals(assemblyName));
+            if (asm == null)
+                throw new InvalidOperationException("Failed to find assembly '" + assemblyName + "'.");
+            var type = asm.GetType(parts[0]);
+            if (type == null)
+                throw new InvalidOperationException("Failed to find type '" + parts[0] + "' in assembly '" +
+                                                    assemblyName + "'.");
+
+            var constructor = type
+                .GetConstructors()
+                .FirstOrDefault(x => x.GetParameters().Length == constructorArguments.Length);
+            if (constructor == null)
+                throw new NotSupportedException(
+                    $"Failed to find constructor for {typeName} with arguments [{string.Join(",", constructorArguments)}]");
+
+            return constructor.Invoke(constructorArguments);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.PluginApi/Incidents/QuickFactContext.cs b/src/Server/Coderr.Server.PluginApi/Incidents/QuickFactContext.cs
new file mode 100644
index 00000000..93790786
--- /dev/null
+++ b/src/Server/Coderr.Server.PluginApi/Incidents/QuickFactContext.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using codeRR.Server.Api.Core.Incidents.Queries;
+
+namespace Coderr.Server.PluginApi.Incidents
+{
+    public class QuickFactContext
+    {
+        public QuickFactContext(int applicationId, int incidentId, ICollection facts)
+        {
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId));
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId));
+            ApplicationId = applicationId;
+            IncidentId = incidentId;
+            CollectedFacts = facts;
+        }
+        public int IncidentId { get; private set; }
+        public int ApplicationId { get; private set; }
+        public ICollection CollectedFacts { get; private set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/ConfigurationContext.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/ConfigurationContext.cs
new file mode 100644
index 00000000..521afec6
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/ConfigurationContext.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Data;
+using System.Security.Claims;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Boot
+{
+    public class ConfigurationContext
+    {
+        public ConfigurationContext(IServiceCollection serviceCollection, Func serviceProviderFactory)
+        {
+            Services= serviceCollection;
+            ServiceProvider = serviceProviderFactory;
+        }
+
+        public Func ConnectionFactory { get; set; }
+        
+        public IServiceCollection Services { get; private set; }
+
+        public Func ServiceProvider { get; private set; }
+
+        public IConfiguration Configuration { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IConfiguration.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IConfiguration.cs
new file mode 100644
index 00000000..207758a2
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IConfiguration.cs
@@ -0,0 +1,12 @@
+using System.Collections.Generic;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Boot
+{
+    public interface IConfiguration
+    {
+        string this[string name] { get; }
+        IEnumerable GetChildren();
+        string GetConnectionString(string name);
+        IConfigurationSection GetSection(string name);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IConfigurationSection.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IConfigurationSection.cs
new file mode 100644
index 00000000..c19644f7
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IConfigurationSection.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Boot
+{
+    /// 
+    /// Abstraction for the .NET Core configuration files.
+    /// 
+    public interface IConfigurationSection
+    {
+        string this[string name] { get; }
+        IEnumerable GetChildren();
+
+        string Value { get; }
+    }
+
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IReportAnalyzerModule.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IReportAnalyzerModule.cs
new file mode 100644
index 00000000..945499e5
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/IReportAnalyzerModule.cs
@@ -0,0 +1,10 @@
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Boot
+{
+    public interface IReportAnalyzerModule
+    {
+        void Configure(ConfigurationContext context);
+        void Start(StartContext context);
+
+        void Stop();
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/StartContext.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/StartContext.cs
new file mode 100644
index 00000000..ff5e586f
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Boot/StartContext.cs
@@ -0,0 +1,9 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Boot
+{
+    public class StartContext
+    {
+        public IServiceProvider ServiceProvider { get; set; }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj
new file mode 100644
index 00000000..d8c75adc
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj
@@ -0,0 +1,39 @@
+
+
+  
+    netstandard2.0
+    2.3.3
+    true
+    Coderr.Server.ReportAnalyzer.Abstractions
+    Debug;Release;Premise
+  
+
+  
+    Coderr.Server.ReportAnalyzer.Abstractions
+    1TCompany AB
+    API client for Coderr Server.
+    true
+    Converted to vstudio 2017 csproj format
+    Copyright 2017 © 1TCompany AB. All rights reserved.
+    logger exceptions analysis .net-core netstandard
+    https://coderr.io/images/nuget_icon.png
+    https://github.com/coderrio/coderr.server
+    git
+    https://raw.githubusercontent.com/coderrio/Coderr.Server/master/LICENSE
+    https://coderr.io
+  
+
+  
+    
+  
+  
+    <_PackageFiles Include="$(OutputPath)\Coderr.Server.Abstractions.*">
+      None
+      lib\netstandard2.0\
+    
+  
+  
+    
+    
+  
+
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntries.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntries.cs
new file mode 100644
index 00000000..9f0c3f76
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntries.cs
@@ -0,0 +1,27 @@
+using System;
+using Coderr.Server.Api;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Commands
+{
+    [Command]
+    public class StoreLogEntries
+    {
+        public StoreLogEntries(int incidentId, int reportId, StoreLogEntriesEntry[] entries)
+        {
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId));
+            if (reportId <= 0) throw new ArgumentOutOfRangeException(nameof(reportId));
+            IncidentId = incidentId;
+            ReportId = reportId;
+            Entries = entries ?? throw new ArgumentNullException(nameof(entries));
+        }
+
+        protected StoreLogEntries()
+        {
+
+        }
+
+        public StoreLogEntriesEntry[] Entries { get; private set; }
+        public int IncidentId { get; private set; }
+        public int ReportId { get; private set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntriesEntry.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntriesEntry.cs
new file mode 100644
index 00000000..19010247
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntriesEntry.cs
@@ -0,0 +1,17 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Commands
+{
+    public class StoreLogEntriesEntry
+    {
+        public DateTime TimeStampUtc { get; set; }
+
+        public string Message { get; set; }
+
+        public StoreLogEntriesLogLevel Level { get; set; }
+
+        public string Exception { get; set; }
+
+        public string Source { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntriesLogLevel.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntriesLogLevel.cs
new file mode 100644
index 00000000..331c44a5
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Commands/StoreLogEntriesLogLevel.cs
@@ -0,0 +1,12 @@
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Commands
+{
+    public enum StoreLogEntriesLogLevel
+    {
+        Trace = 1,
+        Debug = 2,
+        Info = 3,
+        Warning = 4,
+        Error = 5,
+        Critical = 6
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/CollectionExtensions.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/CollectionExtensions.cs
new file mode 100644
index 00000000..9eaebfea
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/CollectionExtensions.cs
@@ -0,0 +1,24 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports
+{
+    public static class ReportExtensions
+    {
+
+        public static ContextCollectionDTO GetCoderrCollection(
+            this IEnumerable instance)
+        {
+            return instance.FirstOrDefault(x => x.Name == "CoderrData");
+        }
+
+        public static ContextCollectionDTO GetCoderrCollection(
+            this ReportDTO instance)
+        {
+            return instance.ContextCollections.FirstOrDefault(x => x.Name == "CoderrData");
+        }
+
+
+
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ContextCollectionDTO.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ContextCollectionDTO.cs
new file mode 100644
index 00000000..0497e1ec
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ContextCollectionDTO.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports
+{
+    /// 
+    ///     Context collection DTO.
+    /// 
+    public class ContextCollectionDTO
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        protected ContextCollectionDTO()
+        {
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Name as specified in the client library
+        /// Properties.
+        public ContextCollectionDTO(string name, IDictionary items)
+        {
+            if (name == null) throw new ArgumentNullException("name");
+            if (items == null) throw new ArgumentNullException("items");
+
+            Name = name;
+            Properties = items;
+        }
+
+
+        /// 
+        ///     Name as specified in the client library
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Properties.
+        /// 
+        public IDictionary Properties { get; set; }
+
+        /// 
+        ///     Returns a string that represents the current object.
+        /// 
+        /// 
+        ///     A string that represents the current object.
+        /// 
+        /// 2
+        public override string ToString()
+        {
+            var flatten = Properties.Select(x => x.Key + "=" + x.Value);
+            var joinProps = string.Join(", ", flatten);
+            return string.Format("{0} [{1}]", Name, joinProps);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/IReportConfig.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/IReportConfig.cs
new file mode 100644
index 00000000..704e6c8d
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/IReportConfig.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports
+{
+    public interface IReportConfig
+    {
+        int MaxReportJsonSize { get; }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ReportDTO.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ReportDTO.cs
new file mode 100644
index 00000000..815893f3
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ReportDTO.cs
@@ -0,0 +1,60 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports
+{
+    /// 
+    ///     Report representation.
+    /// 
+    public class ReportDTO
+    {
+        /// 
+        ///     Application that the incident and report belongs in.
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     A collection of context information such as HTTP request information or computer hardware info.
+        /// 
+        public ContextCollectionDTO[] ContextCollections { get; set; }
+
+        /// 
+        ///     Date specified at client side
+        /// 
+        public DateTime CreatedAtUtc { get; set; }
+
+        /// 
+        ///     Exception which was caught.
+        /// 
+        public ReportExeptionDTO Exception { get; set; }
+
+        /// 
+        ///     DB primary key
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     DB primary key
+        /// 
+        public int IncidentId { get; set; }
+
+        /// 
+        ///     Ip of the report uploader.
+        /// 
+        public string RemoteAddress { get; set; }
+
+        /// 
+        ///     Gets error id (unique identifier used in communication with the customer to identify this error)
+        /// 
+        public string ReportId { get; set; }
+
+        /// 
+        ///     Version of the report
+        /// 
+        public string ReportVersion { get; set; }
+
+        /// 
+        ///     Application version without prefix ("1.0.9" and not "v1.0.9")
+        /// 
+        public string ApplicationVersion { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ReportExeptionDTO.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ReportExeptionDTO.cs
new file mode 100644
index 00000000..4e72f7f8
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ErrorReports/ReportExeptionDTO.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports
+{
+    /// 
+    ///     Model used to wrap all information from an exception.
+    /// 
+    public class ReportExeptionDTO
+    {
+        /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        public ReportExeptionDTO()
+        {
+            Properties = new Dictionary(StringComparer.OrdinalIgnoreCase);
+        }
+
+        /// 
+        ///     Assembly name (version included)
+        /// 
+        public string AssemblyName { get; set; }
+
+        /// 
+        ///     Exception base classes. Most specific first: ArgumentOutOfRangeException, ArgumentException,
+        ///     Exception.
+        /// 
+        public string[] BaseClasses { get; set; }
+
+        /// 
+        ///     Everything (exception.ToString())
+        /// 
+        public string Everything { get; set; }
+
+        /// 
+        ///     Full type name (namespace + class name)
+        /// 
+        public string FullName { get; set; }
+
+        /// 
+        ///     Inner exception (if any; otherwise null).
+        /// 
+        public ReportExeptionDTO InnerException { get; set; }
+
+        /// 
+        ///     Exception message
+        /// 
+        public string Message { get; set; }
+
+        /// 
+        ///     Type name
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Namespace that the exception is in
+        /// 
+        public string Namespace { get; set; }
+
+        /// 
+        ///     All properties (public and private)
+        /// 
+        public IDictionary Properties { get; set; }
+
+        /// 
+        ///     Stack trace, line numbers included if your app also distributes the PDB files.
+        /// 
+        public string StackTrace { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Feedback/FeedbackAttachedToIncident.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Feedback/FeedbackAttachedToIncident.cs
new file mode 100644
index 00000000..061d998e
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Feedback/FeedbackAttachedToIncident.cs
@@ -0,0 +1,33 @@
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Feedback
+{
+    /// 
+    ///     Feedback was attached to incident.
+    /// 
+    public class FeedbackAttachedToIncident
+    {
+        /// 
+        ///     Application that the incident belongs to.
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Name of the application that the feedback is for.
+        /// 
+        public string ApplicationName { get; set; }
+
+        /// 
+        ///     Incident that the feedback was attached to.
+        /// 
+        public int IncidentId { get; set; }
+
+        /// 
+        ///     Feedback message.
+        /// 
+        public string Message { get; set; }
+
+        /// 
+        ///     Email address to the user that wrote the message (optional)
+        /// 
+        public string UserEmailAddress { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/IDomainQueue.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/IDomainQueue.cs
new file mode 100644
index 00000000..9bee3052
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/IDomainQueue.cs
@@ -0,0 +1,18 @@
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions
+{
+    /// 
+    ///     PublishAsync messages into the domain queue
+    /// 
+    /// 
+    ///     
+    ///         Used by report analyzers to send commands/events into the queue that the application uses.
+    ///     
+    /// 
+    public interface IDomainQueue : ISaveable
+    {
+        Task PublishAsync(ClaimsPrincipal principal, object message);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ISaveable.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ISaveable.cs
new file mode 100644
index 00000000..7148cd0f
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/ISaveable.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions
+{
+    /// 
+    /// Class can be used to commit a unit of work.
+    /// 
+    public interface ISaveable
+    {
+        Task SaveChanges();
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/InvalidReport.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/InvalidReport.cs
similarity index 93%
rename from src/Server/OneTrueError.ReportAnalyzer/LibContracts/InvalidReport.cs
rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/InvalidReport.cs
index b4e4a729..309d4102 100644
--- a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/InvalidReport.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/InvalidReport.cs
@@ -1,41 +1,41 @@
-using System;
-
-namespace OneTrueError.ReportAnalyzer.LibContracts
-{
-    /// 
-    ///     Failed to identify an incoming report. Stored for debugging purposes.
-    /// 
-    public class InvalidReport
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// Application key that the client sent
-        /// Report exactly as we received it (unpacked)
-        /// Exception thrown when we tried to unpack and store it.
-        public InvalidReport(string appKey, byte[] body, Exception exception)
-        {
-            if (appKey == null) throw new ArgumentNullException("appKey");
-            if (body == null) throw new ArgumentNullException("body");
-
-            AppKey = appKey;
-            Body = body;
-            Exception = exception;
-        }
-
-        /// 
-        ///     Application key that the client sent
-        /// 
-        public string AppKey { get; set; }
-
-        /// 
-        ///     Report body
-        /// 
-        public byte[] Body { get; set; }
-
-        /// 
-        ///     Exception thrown when we tried to unpack and store the report.
-        /// 
-        public Exception Exception { get; private set; }
-    }
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands
+{
+    /// 
+    ///     Failed to identify an incoming report. Stored for debugging purposes.
+    /// 
+    public class InvalidReport
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Application key that the client sent
+        /// Report exactly as we received it (unpacked)
+        /// Exception thrown when we tried to unpack and store it.
+        public InvalidReport(string appKey, byte[] body, Exception exception)
+        {
+            if (appKey == null) throw new ArgumentNullException("appKey");
+            if (body == null) throw new ArgumentNullException("body");
+
+            AppKey = appKey;
+            Body = body;
+            Exception = exception;
+        }
+
+        /// 
+        ///     Application key that the client sent
+        /// 
+        public string AppKey { get; set; }
+
+        /// 
+        ///     Report body
+        /// 
+        public byte[] Body { get; set; }
+
+        /// 
+        ///     Exception thrown when we tried to unpack and store the report.
+        /// 
+        public Exception Exception { get; private set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/NamespaceDoc.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/NamespaceDoc.cs
new file mode 100644
index 00000000..1aeeeab9
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/NamespaceDoc.cs
@@ -0,0 +1,15 @@
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands
+{
+    /// 
+    ///     These classes are created by the "Receiver" area when new reports are being received.
+    /// 
+    /// 
+    ///     
+    ///         i.e. they contain our internal normalization to not let the internal processing be affected by
+    ///         API changes in the client libraries.
+    ///     
+    /// 
+    internal class NamespaceDoc
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessFeedback.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessFeedback.cs
new file mode 100644
index 00000000..dda2d037
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessFeedback.cs
@@ -0,0 +1,58 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands
+{
+    /// 
+    ///     Feedback item as received by the client library
+    /// 
+    [Serializable]
+    public class ProcessFeedback
+    {
+        /// 
+        ///     Application that the report belongs to.
+        /// 
+        /// 
+        ///     Added when the application is identified in the server.
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     What the user typed about what he did when the exception occurred.
+        /// 
+        /// 
+        ///     From the library contract
+        /// 
+        public string Description { get; set; }
+
+        /// 
+        ///     User want to get status updates.
+        /// 
+        /// 
+        ///     From the library contract
+        /// 
+        public string EmailAddress { get; set; }
+
+        /// 
+        ///     When the report receiver stored this report
+        /// 
+        public DateTime ReceivedAtUtc { get; set; }
+
+        /// 
+        ///     Remote address that we received the report from
+        /// 
+        public string RemoteAddress { get; set; }
+
+        /// 
+        ///     Unique id for this report.
+        /// 
+        /// 
+        ///     From the library contract
+        /// 
+        public string ReportId { get; set; }
+
+        /// 
+        ///     Version of the report (version of the Coderr.Reporting API contract)
+        /// 
+        public string ReportVersion { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReport.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReport.cs
new file mode 100644
index 00000000..2fc745ab
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReport.cs
@@ -0,0 +1,62 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands
+{
+    /// 
+    ///     Command
+    /// 
+    public class ProcessReport
+    {
+        /// 
+        ///     Application that this report belongs to
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     A collection of context information such as HTTP request information or computer hardware info.
+        /// 
+        public ProcessReportContextInfoDto[] ContextCollections { get; set; }
+
+        /// 
+        ///     Date specified at client side
+        /// 
+        public DateTime CreatedAtUtc { get; set; }
+
+        /// 
+        ///     When the report receiver stored this report
+        /// 
+        public DateTime DateReceivedUtc { get; set; }
+
+
+        /// 
+        ///     Exception which was caught.
+        /// 
+        public ProcessReportExceptionDto Exception { get; set; }
+
+        /// 
+        /// 100 latest log entries if attached.
+        /// 
+        public ProcessReportLogEntry[] LogEntries { get; set; }
+
+        /// 
+        ///     Remote address that we received the report from
+        /// 
+        public string RemoteAddress { get; set; }
+
+        /// 
+        ///     Gets incident id (unique identifier used in communication with the customer to identify this error)
+        /// 
+        public string ReportId { get; set; }
+
+        /// 
+        ///     Version of the report (version of the Coderr.Reporting API contract)
+        /// 
+        public string ReportVersion { get; set; }
+
+
+        /// 
+        /// "Dev", "Production", "Test" etc
+        /// 
+        public string EnvironmentName { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportContextInfoDto.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportContextInfoDto.cs
new file mode 100644
index 00000000..0d25c6dc
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportContextInfoDto.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands
+{
+    /// 
+    ///     Context collection
+    /// 
+    [Serializable]
+    public class ProcessReportContextInfoDto
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        protected ProcessReportContextInfoDto()
+        {
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// context collection name
+        /// properties
+        /// name; items
+        public ProcessReportContextInfoDto(string name, Dictionary properties)
+        {
+            if (name == null) throw new ArgumentNullException("name");
+            if (properties == null) throw new ArgumentNullException("properties");
+
+            Name = name;
+            Properties = properties;
+        }
+
+
+        /// 
+        ///     Context collection name
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Properties
+        /// 
+        public Dictionary Properties { get; set; }
+
+        /// 
+        ///     Returns a string that represents the current object.
+        /// 
+        /// 
+        ///     A string that represents the current object.
+        /// 
+        /// 2
+        public override string ToString()
+        {
+            return Name + " [" + string.Join(", ",
+                Properties.Select(x => x.Key + "=" + x.Value)) + "]";
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportExceptionDto.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportExceptionDto.cs
new file mode 100644
index 00000000..d0e45597
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportExceptionDto.cs
@@ -0,0 +1,71 @@
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands
+{
+    /// 
+    ///     Model used to wrap all information from an exception.
+    /// 
+    [Serializable]
+    public class ProcessReportExceptionDto
+    {
+        /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        public ProcessReportExceptionDto()
+        {
+            Properties = new Dictionary(StringComparer.OrdinalIgnoreCase);
+        }
+
+        /// 
+        ///     Assembly name (version included)
+        /// 
+        public string AssemblyName { get; set; }
+
+        /// 
+        ///     Exception base classes. Most specific first: ArgumentOutOfRangeException, ArgumentException,
+        ///     Exception.
+        /// 
+        public string[] BaseClasses { get; set; }
+
+        /// 
+        ///     Everything (exception.ToString())
+        /// 
+        public string Everything { get; set; }
+
+        /// 
+        ///     Full type name (namespace + class name)
+        /// 
+        public string FullName { get; set; }
+
+        /// 
+        ///     Inner exception (if any; otherwise null).
+        /// 
+        public ProcessReportExceptionDto InnerExceptionDto { get; set; }
+
+        /// 
+        ///     Exception message
+        /// 
+        public string Message { get; set; }
+
+        /// 
+        ///     Type name
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Namespace that the exception is in
+        /// 
+        public string Namespace { get; set; }
+
+        /// 
+        ///     All properties (public and private)
+        /// 
+        public IDictionary Properties { get; set; }
+
+        /// 
+        ///     Stack trace, line numbers included if your app also distributes the PDB files.
+        /// 
+        public string StackTrace { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportLogEntry.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportLogEntry.cs
new file mode 100644
index 00000000..f9a37a93
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ProcessReportLogEntry.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands
+{
+    /// 
+    /// Process (analyze) a new error report.
+    /// 
+    public class ProcessReportLogEntry
+    {
+        /// 
+        /// When this log entry was written
+        /// 
+        public DateTime TimestampUtc { get; set; }
+
+        /// 
+        /// 0 = trace, 1 = debug, 2 = info, 3 = warning, 4 = error, 5 = critical
+        /// 
+        public int LogLevel { get; set; }
+
+        /// 
+        /// Logged message
+        /// 
+        public string Message { get; set; }
+
+        /// 
+        /// Exception as string (if any was attached to this log entry)
+        /// 
+        public string Exception { get; set; }
+
+        /// 
+        /// Where in the code that the log entry is from (class name, method name or similar)
+        /// 
+        public string Source { get; set; }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ReadMe.md b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ReadMe.md
new file mode 100644
index 00000000..321308d3
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Commands/ReadMe.md
@@ -0,0 +1 @@
+Commands used to store inbound error reports for processing.
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NamespaceDoc.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NamespaceDoc.cs
new file mode 100644
index 00000000..173bfe87
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NamespaceDoc.cs
@@ -0,0 +1,22 @@
+using System.Runtime.CompilerServices;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models
+{
+    /// 
+    ///     Used by the ReportReceiver to receive error reports and store them in the internal queue. Picked up by the report analyzer for analysis.
+    /// 
+    /// 
+    ///     
+    ///         Do not modify these when the client library changes (unless the change is backwards compatible), instead create
+    ///         a new class
+    ///         which the changes are applied to and name it with the same version number that is sent by the client.
+    ///     
+    ///     
+    ///         In that way we can support multiple API versions.
+    ///     
+    /// 
+    [CompilerGenerated]
+    public class NamespaceDoc
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportContextInfo.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportContextInfo.cs
new file mode 100644
index 00000000..49f30027
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportContextInfo.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models
+{
+    public class NewReportContextInfo
+    {
+        public NewReportContextInfo()
+        {
+        }
+
+        public NewReportContextInfo(string name, Dictionary properties)
+        {
+            Name = name ?? throw new ArgumentNullException(nameof(name));
+            Properties = properties ?? throw new ArgumentNullException(nameof(properties));
+        }
+
+
+        public string Name { get; set; }
+
+        public Dictionary Properties { get; set; }
+
+        public override string ToString()
+        {
+            return Name + " [" + string.Join(", ",
+                Properties.Select(x => x.Key + "=" + x.Value)) + "]";
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportDTO.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportDTO.cs
new file mode 100644
index 00000000..222d83ff
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportDTO.cs
@@ -0,0 +1,53 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models
+{
+    /// 
+    ///     Report as uploaded by the client API (should always match the client library contracts, but with with own additions like RemoteIp).
+    /// 
+    public class NewReportDTO
+    {
+        /// 
+        ///     A collection of context information such as HTTP request information or computer hardware info.
+        /// 
+        public NewReportContextInfo[] ContextCollections { get; set; }
+
+        /// 
+        ///     Date specified at client side
+        /// 
+        public DateTime CreatedAtUtc { get; set; }
+
+
+        /// 
+        ///     Exception which was caught.
+        /// 
+        public NewReportException Exception { get; set; }
+
+        /// 
+        /// The 100 last log entries before the exception was detected (can be null).
+        /// 
+        public NewReportLogEntry[] LogEntries { get; set; }
+
+        /// 
+        /// "Dev", "Production" etc.
+        /// 
+        public string EnvironmentName { get; set; }
+
+        /// 
+        /// Used in older clients.
+        /// 
+        public string Environment { get; set; }
+
+        public string RemoteAddress { get; set; }
+
+        /// 
+        ///     Gets incident id (unique identifier used in communication with the customer to identify this error)
+        /// 
+        public string ReportId { get; set; }
+
+        /// 
+        ///     Version of the report
+        /// 
+        public string ReportVersion { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportException.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportException.cs
similarity index 91%
rename from src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportException.cs
rename to src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportException.cs
index a1c6dd78..c0483718 100644
--- a/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportException.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportException.cs
@@ -1,69 +1,70 @@
-using System.Collections.Generic;
-
-namespace OneTrueError.Web.Areas.Receiver.ReportingApi
-{
-    /// 
-    ///     Model used to wrap all information from an exception.
-    /// 
-    public class NewReportException
-    {
-        /// 
-        ///     Initializes a new instance of the  class.
-        /// 
-        public NewReportException()
-        {
-            Properties = new Dictionary();
-        }
-
-        /// 
-        ///     Assembly name (version included)
-        /// 
-        public string AssemblyName { get; set; }
-
-        /// 
-        ///     Exception base classes. Most specific first: ArgumentOutOfRangeException, ArgumentException,
-        ///     Exception.
-        /// 
-        public string[] BaseClasses { get; set; }
-
-        /// 
-        ///     Everything (exception.ToString())
-        /// 
-        public string Everything { get; set; }
-
-        /// 
-        ///     Full type name (namespace + class name)
-        /// 
-        public string FullName { get; set; }
-
-        /// 
-        ///     Inner exception (if any; otherwise null).
-        /// 
-        public NewReportException InnerException { get; set; }
-
-        /// 
-        ///     Exception message
-        /// 
-        public string Message { get; set; }
-
-        /// 
-        ///     Type name
-        /// 
-        public string Name { get; set; }
-
-        /// 
-        ///     Namespace that the exception is in
-        /// 
-        public string Namespace { get; set; }
-
-        /// 
-        ///     All properties (public and private)
-        /// 
-        public IDictionary Properties { get; set; }
-
-        /// 
-        ///     Stack trace, line numbers included if your app also distributes the PDB files.
-        /// 
-        public string StackTrace { get; set; }
-    }
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models
+{
+    /// 
+    ///     Model used to wrap all information from an exception.
+    /// 
+    public class NewReportException
+    {
+        /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        public NewReportException()
+        {
+            Properties = new Dictionary(StringComparer.OrdinalIgnoreCase);
+        }
+
+        /// 
+        ///     Assembly name (version included)
+        /// 
+        public string AssemblyName { get; set; }
+
+        /// 
+        ///     Exception base classes. Most specific first: ArgumentOutOfRangeException, ArgumentException,
+        ///     Exception.
+        /// 
+        public string[] BaseClasses { get; set; }
+
+        /// 
+        ///     Everything (exception.ToString())
+        /// 
+        public string Everything { get; set; }
+
+        /// 
+        ///     Full type name (namespace + class name)
+        /// 
+        public string FullName { get; set; }
+
+        /// 
+        ///     Inner exception (if any; otherwise null).
+        /// 
+        public NewReportException InnerException { get; set; }
+
+        /// 
+        ///     Exception message
+        /// 
+        public string Message { get; set; }
+
+        /// 
+        ///     Type name
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Namespace that the exception is in
+        /// 
+        public string Namespace { get; set; }
+
+        /// 
+        ///     All properties (public and private)
+        /// 
+        public IDictionary Properties { get; set; }
+
+        /// 
+        ///     Stack trace, line numbers included if your app also distributes the PDB files.
+        /// 
+        public string StackTrace { get; set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportLogEntry.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportLogEntry.cs
new file mode 100644
index 00000000..85339145
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Inbound/Models/NewReportLogEntry.cs
@@ -0,0 +1,36 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models
+{
+    /// 
+    /// Logentry for .
+    /// 
+    public class NewReportLogEntry
+    {
+
+        /// 
+        /// When this log entry was written
+        /// 
+        public DateTime TimestampUtc { get; set; }
+
+        /// 
+        /// 0 = trace, 1 = debug, 2 = info, 3 = warning, 4 = error, 5 = critical
+        /// 
+        public int LogLevel { get; set; }
+
+        /// 
+        /// Logged message
+        /// 
+        public string Message { get; set; }
+
+        /// 
+        /// Exception as string (if any was attached to this log entry)
+        /// 
+        public string Exception { get; set; }
+
+        /// 
+        /// Location in the code that generated this log entry. Can be null.
+        /// 
+        public string Source { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/IncidentSummaryDTO.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/IncidentSummaryDTO.cs
new file mode 100644
index 00000000..72ff873b
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/IncidentSummaryDTO.cs
@@ -0,0 +1,80 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Incidents
+{
+    /// 
+    ///     A small summary of an incident, typically used to list incidents.
+    /// 
+    public class IncidentSummaryDTO
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// incident id
+        /// incident name
+        /// name
+        /// incident id
+        public IncidentSummaryDTO(int id, string name)
+        {
+            if (name == null) throw new ArgumentNullException("name");
+            if (id <= 0) throw new ArgumentOutOfRangeException("id");
+
+            Id = id;
+            Name = name;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected IncidentSummaryDTO()
+        {
+        }
+
+        /// 
+        ///     Application that the incident belongs to
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Name of that application
+        /// 
+        public string ApplicationName { get; set; }
+
+        /// 
+        ///     When the incident was created (when we received the first report).
+        /// 
+        public DateTime CreatedAtUtc { get; set; }
+
+        /// 
+        ///     Incident id
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     Incident was closed but then received a new error report.
+        /// 
+        public bool IsReOpened { get; set; }
+
+        /// 
+        /// someone is assigned to this incident
+        /// 
+        public int? AssignedToUserId { get; set; }
+
+        /// 
+        ///     Update is both when the incident was open/closed and when we received a new report. TODO: Should be refactored into
+        ///     two fields.
+        /// 
+        public DateTime LastUpdateAtUtc { get; set; }
+
+        /// 
+        ///     Incident name (typically first line of the exception message)
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Number of reports that we've received. Should be the total amount (including those that have been deleted due to
+        ///     retention days).
+        /// 
+        public int ReportCount { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/ProcessContextCollections.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/ProcessContextCollections.cs
new file mode 100644
index 00000000..31cd087e
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/ProcessContextCollections.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Incidents
+{
+    /// 
+    /// Job used to scan inbound collections and process them.
+    /// 
+    public class ProcessInboundContextCollections
+    {
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/ReportAddedToIncident.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/ReportAddedToIncident.cs
new file mode 100644
index 00000000..699f906c
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Incidents/ReportAddedToIncident.cs
@@ -0,0 +1,82 @@
+using System;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Incidents
+{
+    /// 
+    ///     We just received a new report and attached it to the given incident.
+    /// 
+    public class ReportAddedToIncident
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// incident that the report was added to
+        /// received report
+        /// Incident was marked as closed, so the received report opened it again.
+        /// incident;report
+        public ReportAddedToIncident(IncidentSummaryDTO incident, ReportDTO report, bool isReOpened)
+        {
+            if (incident == null)
+            {
+                throw new ArgumentNullException("incident");
+            }
+
+            if (report == null)
+            {
+                throw new ArgumentNullException("report");
+            }
+
+            Incident = incident;
+            Report = report;
+            IsReOpened = isReOpened;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected ReportAddedToIncident()
+        {
+        }
+
+
+        /// 
+        ///     Which environment the error was received in.
+        /// 
+        public string EnvironmentName { get; set; }
+
+        /// 
+        ///     Incident that the report was added to.
+        /// 
+        public IncidentSummaryDTO Incident { get; private set; }
+
+        /// 
+        ///     Is the incident new, i.e. this is the first report for an incident.
+        /// 
+        /// 
+        ///     New parameter in later versions = nullable.
+        /// 
+        public bool? IsNewIncident { get; set; }
+
+        /// 
+        ///     Incident was marked as closed, so the received report opened it again.
+        /// 
+        public bool IsReOpened { get; set; }
+
+        /// 
+        ///     Report have been stored (i.e. we do not have too many reports yet).
+        /// 
+        /// 
+        ///     
+        ///         If false, the report will get discarded which also means that all analyzes that points to it will fail.
+        ///         Therefore, make sure that all analytics run on this report is incident specific and not report specific.
+        ///     
+        /// 
+        public bool? IsStored { get; set; }
+
+        /// 
+        ///     Received report.
+        /// 
+        public ReportDTO Report { get; private set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Notifications/Commands/SendBrowserNotification.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Notifications/Commands/SendBrowserNotification.cs
new file mode 100644
index 00000000..1805416c
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Notifications/Commands/SendBrowserNotification.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Notifications.Commands
+{
+    public class SendBrowserNotification
+    {
+        public SendBrowserNotification(int accountIdToSendTo)
+        {
+            if (accountIdToSendTo <= 0) throw new ArgumentOutOfRangeException(nameof(accountIdToSendTo));
+            AccountIdToSendTo = accountIdToSendTo;
+        }
+
+        public int AccountIdToSendTo { get; private set; }
+
+        public IList Actions { get; set; } =
+            new List();
+
+        public string Badge { get; set; }
+
+        public string Body { get; set; }
+
+
+        public string IconUrl { get; set; }
+
+        public string ImageUrl { get; set; }
+
+        public string LanguageCode { get; set; } = "en";
+
+        public bool RequireInteraction { get; set; }
+
+        public string Tag { get; set; }
+
+        public DateTime Timestamp { get; set; } = DateTime.Now;
+
+        public string Title { get; set; } = "Push Demo";
+
+        /// 
+        ///     Anonymous object
+        /// 
+        public object UserData { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Notifications/Commands/SendBrowserNotificationAction.cs b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Notifications/Commands/SendBrowserNotificationAction.cs
new file mode 100644
index 00000000..5cdebf23
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Abstractions/Notifications/Commands/SendBrowserNotificationAction.cs
@@ -0,0 +1,12 @@
+namespace Coderr.Server.ReportAnalyzer.Abstractions.Notifications.Commands
+{
+    public class SendBrowserNotificationAction
+    {
+        /// 
+        ///     Method name in the service worker script.
+        /// 
+        public string Action { get; set; }
+
+        public string Title { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/ApplicationVersions/Handlers/GetVersionFromReportTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ApplicationVersions/Handlers/GetVersionFromReportTests.cs
new file mode 100644
index 00000000..e4f5c415
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ApplicationVersions/Handlers/GetVersionFromReportTests.cs
@@ -0,0 +1,29 @@
+using Coderr.Server.ReportAnalyzer.ApplicationVersions.Handlers;
+using FluentAssertions;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.ApplicationVersions.Handlers
+{
+    public class GetVersionFromReportTests
+    {
+        [Fact]
+        public void Should_keep_one_zero_for_exact_majors()
+        {
+            var expected = "1.0";
+
+            var actual = GetVersionFromReport.SimplifyVersion("1.0.0.0");
+
+            actual.Should().Be(expected);
+        }
+
+        [Fact]
+        public void Should_trim_end_zeros_but_leave_other_digits()
+        {
+            var expected = "3.1";
+
+            var actual = GetVersionFromReport.SimplifyVersion("3.1.0.0");
+
+            actual.Should().Be(expected);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/Coderr.Server.ReportAnalyzer.Tests.csproj b/src/Server/Coderr.Server.ReportAnalyzer.Tests/Coderr.Server.ReportAnalyzer.Tests.csproj
new file mode 100644
index 00000000..abcb060d
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/Coderr.Server.ReportAnalyzer.Tests.csproj
@@ -0,0 +1,28 @@
+
+  
+    netcoreapp3.1
+    true
+    true
+  
+
+  
+    
+    
+    
+      all
+      runtime; build; native; contentfiles; analyzers; buildtransitive
+    
+    
+    
+  
+
+  
+    
+    
+  
+
+  
+    
+  
+
+
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/Domain/Reports/HashCodeGeneratorTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/Domain/Reports/HashCodeGeneratorTests.cs
new file mode 100644
index 00000000..5b93b735
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/Domain/Reports/HashCodeGeneratorTests.cs
@@ -0,0 +1,208 @@
+using System;
+using System.Collections.Generic;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.ErrorReports;
+using FluentAssertions;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.Domain.Reports
+{
+    public class HashCodeGeneratorTests
+    {
+        [Fact]
+        public void Should_strip_line_numbers_to_reduce_the_number_of_incidents()
+        {
+            var ex1 = new ErrorReportException { StackTrace = @"   at System.Web.Compilation.AssemblyBuilder.Compile():line 64
+at System.Web.Compilation.BuildProvidersCompiler.PerformBuild():line 65
+at System.Web.Compilation.BuildManager.CompileWebFile(VirtualPath virtualPath):line 67" };
+            var ex2 = new ErrorReportException
+            {
+                StackTrace = @"   at System.Web.Compilation.AssemblyBuilder.Compile():line 12
+at System.Web.Compilation.BuildProvidersCompiler.PerformBuild():line 23
+at System.Web.Compilation.BuildManager.CompileWebFile(VirtualPath virtualPath):line 53"
+            };
+            var report1 = new ErrorReportEntity(1, "fjkkfjjkf", DateTime.UtcNow, ex1, new List());
+            var report2 = new ErrorReportEntity(1, "fjkkfjjkf", DateTime.UtcNow, ex2, new List());
+
+            var sut = new HashCodeGenerator(new IHashCodeSubGenerator[0]);
+            var actual1 = sut.GenerateHashCode(report1);
+            var actual2 = sut.GenerateHashCode(report2);
+
+            actual1.HashCode.Should().Be(actual2.HashCode);
+        }
+
+        [Fact]
+        public void should_match_line_numbers_and_without_line_numbers_to_reduce_the_number_of_incidents()
+        {
+            var ex1 = new ErrorReportException { StackTrace = @"at System.Web.Compilation.AssemblyBuilder.Compile():line 64
+at System.Web.Compilation.BuildProvidersCompiler.PerformBuild():line 65
+at System.Web.Compilation.BuildManager.CompileWebFile(VirtualPath virtualPath):line 67" };
+            var ex2 = new ErrorReportException
+            {
+                StackTrace = @"at System.Web.Compilation.AssemblyBuilder.Compile()
+at System.Web.Compilation.BuildProvidersCompiler.PerformBuild()
+at System.Web.Compilation.BuildManager.CompileWebFile(VirtualPath virtualPath)"
+            };
+            var report1 = new ErrorReportEntity(1, "fjkkfjjkf", DateTime.UtcNow, ex1, new List());
+            var report2 = new ErrorReportEntity(1, "fjkkfjjkf", DateTime.UtcNow, ex2, new List());
+
+            var sut = new HashCodeGenerator(new IHashCodeSubGenerator[0]);
+            var actual1 = sut.GenerateHashCode(report1);
+            var actual2 = sut.GenerateHashCode(report2);
+
+            actual1.HashCode.Should().Be(actual2.HashCode);
+        }
+
+
+        [Fact]
+        public void Should_clean_aspnet()
+        {
+            var stacktrace = @"   at System.Text.Encoding.GetBytes(String s)
+   at Coderr.Server.Web.Controllers.OnboardingController.CalculateMd5Hash(String input) in C:\src\1tcompany\coderr\oss\codeRR.Server\src\Server\Coderr.Server.Web\Controllers\OnboardingController.cs:line 47
+   at Coderr.Server.Web.Controllers.OnboardingController.Library(String id, String appKey)
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync()
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
+   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync()";
+
+            var sut = new HashCodeGenerator(new IHashCodeSubGenerator[0]);
+            var data = HashCodeGenerator.CleanStackTrace(stacktrace);
+
+            data.Should().Be(@"System.Text.Encoding.GetBytes(String s)
+Coderr.Server.Web.Controllers.OnboardingController.CalculateMd5Hash(String input) in C:\src\1tcompany\coderr\oss\codeRR.Server\src\Server\Coderr.Server.Web\Controllers\OnboardingController.cs
+Coderr.Server.Web.Controllers.OnboardingController.Library(String id, String appKey)");
+        }
+        [Fact]
+        public void Should_clean_async()
+        {
+            var stacktrace = @"   at System.Data.SqlClient.SqlCommand.<>c.b__108_0(Task`1 result)
+   at System.Threading.Tasks.ContinuationResultTaskFromResultTask`2.InnerInvoke()
+   at System.Threading.Tasks.Task.<>c.<.cctor>b__276_1(Object obj)
+   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
+   at System.Threading.Tasks.Task.ExecuteWithThreadLocal(Task& currentTaskSlot)
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
+   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
+   at Coderr.Server.SqlServer.Core.Incidents.Queries.GetCollectionHandler.d__2.MoveNext() in D:\AgentWork\10\s\src\Server\Coderr.Server.SqlServer\Core\Incidents\Queries\GetCollectionHandler.cs:line 46
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
+   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
+   at DotNetCqs.DependencyInjection.MessageInvoker.d__22.MoveNext()
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at DotNetCqs.DependencyInjection.MessageInvoker.d__22.MoveNext()
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
+   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
+   at DotNetCqs.DependencyInjection.MessageInvoker.d__9.MoveNext()
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
+   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
+   at DotNetCqs.MessageProcessor.ExecuteQueriesInvocationContext.d__13`1.MoveNext()
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
+   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
+   at DotNetCqs.Bus.ScopedQueryBus.d__3`1.MoveNext()
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
+   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
+   at Coderr.Server.Common.App.HighlightedData.CustomContextDataProvider.d__2.MoveNext() in D:\AgentWork\10\s\src\Server\Common\Coderr.Server.Common.App\HighlightedData\CustomContextDataProvider.cs:line 27
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
+   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
+   at Coderr.Server.SqlServer.Core.Incidents.Queries.GetIncidentHandler.d__6.MoveNext() in D:\AgentWork\10\s\src\Server\Coderr.Server.SqlServer\Core\Incidents\Queries\GetIncidentHandler.cs:line 107
+--- End of stack trace from previous location where exception was thrown ---
+   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
+   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
+   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
+   at DotNetCqs.DependencyInjection.MessageInvoker.d__22.MoveNext()
+";
+
+            var sut = new HashCodeGenerator(new IHashCodeSubGenerator[0]);
+            var data = HashCodeGenerator.CleanStackTrace(stacktrace);
+
+            data.Should().Be(
+                @"System.Data.SqlClient.SqlCommand.<>c.b__108_0(Task`1 result)
+Coderr.Server.SqlServer.Core.Incidents.Queries.GetCollectionHandler.HandleAsync() in D:\AgentWork\10\s\src\Server\Coderr.Server.SqlServer\Core\Incidents\Queries\GetCollectionHandler.cs
+DotNetCqs.DependencyInjection.MessageInvoker.InvokeQueryHandler()
+DotNetCqs.DependencyInjection.MessageInvoker.InvokeQueryHandler()
+DotNetCqs.DependencyInjection.MessageInvoker.ProcessAsync()
+DotNetCqs.MessageProcessor.ExecuteQueriesInvocationContext.QueryAsync()
+DotNetCqs.Bus.ScopedQueryBus.QueryAsync()
+Coderr.Server.Common.App.HighlightedData.CustomContextDataProvider.CollectAsync() in D:\AgentWork\10\s\src\Server\Common\Coderr.Server.Common.App\HighlightedData\CustomContextDataProvider.cs
+Coderr.Server.SqlServer.Core.Incidents.Queries.GetIncidentHandler.HandleAsync() in D:\AgentWork\10\s\src\Server\Coderr.Server.SqlServer\Core\Incidents\Queries\GetIncidentHandler.cs
+DotNetCqs.DependencyInjection.MessageInvoker.InvokeQueryHandler()");
+        }
+
+        [Fact]
+        public void Should_ignore_java_lambdas()
+        {
+            var ex1 = new ErrorReportException { StackTrace = @"at java.lang.NumberFormatException->forInputString(null:-1)
+at java.lang.Integer->parseInt(null:-1)
+at java.lang.Integer->parseInt(null:-1)
+at se.fleetech.gateway.app.services.alarms.EngineCoolantTemperatureAlarmStateChangeServiceImpl->handleET1Message(EngineCoolantTemperatureAlarmStateChangeServiceImpl.java:110)
+at se.fleetech.gateway.app.services.alarms.EngineCoolantTemperatureAlarmStateChangeServiceImpl->actorCallback(EngineCoolantTemperatureAlarmStateChangeServiceImpl.java:58)
+at se.fleetech.gateway.app.services.alarms.EngineCoolantTemperatureAlarmStateChangeServiceImpl$$Lambda$19/30866698->accept(null:-1)
+at se.fleetech.gateway.app.services.general.ActorService->lambda$start$0(ActorService.java:45)
+at se.fleetech.gateway.app.services.general.ActorService$$Lambda$36/31222037->run(null:-1)
+at java.util.concurrent.ThreadPoolExecutor->runWorker(null:-1)
+at java.util.concurrent.ThreadPoolExecutor$Worker->run(null:-1)
+at java.lang.Thread->run(null:-1)" };
+            var ex2 = new ErrorReportException
+            {
+                StackTrace = @"java.lang.NumberFormatException->forInputString(null:-1)
+java.lang.Integer->parseInt(null:-1)
+java.lang.Integer->parseInt(null:-1)
+se.fleetech.gateway.app.services.alarms.EngineCoolantTemperatureAlarmStateChangeServiceImpl->handleET1Message(EngineCoolantTemperatureAlarmStateChangeServiceImpl.java:112)
+se.fleetech.gateway.app.services.alarms.EngineCoolantTemperatureAlarmStateChangeServiceImpl->actorCallback(EngineCoolantTemperatureAlarmStateChangeServiceImpl.java:62)
+se.fleetech.gateway.app.services.alarms.EngineCoolantTemperatureAlarmStateChangeServiceImpl$$Lambda$19/14039178->accept(null:-1)
+se.fleetech.gateway.app.services.general.ActorService->lambda$start$0(ActorService.java:45)
+se.fleetech.gateway.app.services.general.ActorService$$Lambda$36/12952319->run(null:-1)
+java.util.concurrent.ThreadPoolExecutor->runWorker(null:-1)
+java.util.concurrent.ThreadPoolExecutor$Worker->run(null:-1)
+java.lang.Thread->run(null:-1)"
+            };
+
+            var report1 = new ErrorReportEntity(1, "fjkkfjjkf", DateTime.UtcNow, ex1, new List());
+            var report2 = new ErrorReportEntity(1, "dddwqw", DateTime.UtcNow, ex2, new List());
+
+            var sut = new HashCodeGenerator(new IHashCodeSubGenerator[0]);
+            var actual1 = sut.GenerateHashCode(report1);
+            var actual2 = sut.GenerateHashCode(report2);
+
+            actual1.HashCode.Should().Be(actual2.HashCode);
+        }
+
+        [Fact]
+        public void Should_remove_dotNet_lambdas()
+        {
+            var strackTrace =
+                @"at MvcDemo.Controllers.HomeController.Index(PostModel model) in E:\src\1tcompany\coderr\Demos\csharp\Web\ASP.NET Core Mvc\MvcDemo\MvcDemo\Controllers\HomeController.cs:line 31
+   at lambda_method26(Closure , Object , Object[] )
+   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
+   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
+   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
+   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()
+--- End of stack trace from previous location ---
+   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Rethrow(ActionExecutedContextSealed context)
+   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
+   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
+--- End of stack trace from previous location ---
+   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.g__Awaited|26_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)";
+
+            var cleaned = HashCodeGenerator.CleanStackTrace(strackTrace);
+
+            var actual = @"MvcDemo.Controllers.HomeController.Index(PostModel model) in E:\src\1tcompany\coderr\Demos\csharp\Web\ASP.NET Core Mvc\MvcDemo\MvcDemo\Controllers\HomeController.cs";
+            cleaned.Should().Be(actual);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorOrigins/Handlers/StoreLocationFromNewReportTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorOrigins/Handlers/StoreLocationFromNewReportTests.cs
new file mode 100644
index 00000000..102113e8
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorOrigins/Handlers/StoreLocationFromNewReportTests.cs
@@ -0,0 +1,59 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Domain.Modules.ErrorOrigins;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using Coderr.Server.ReportAnalyzer.ErrorOrigins;
+using Coderr.Server.ReportAnalyzer.ErrorOrigins.Handlers;
+using DotNetCqs;
+using FluentAssertions;
+using NSubstitute;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.ErrorOrigins.Handlers
+{
+    public class StoreLocationFromNewReportTests
+    {
+        [Fact]
+        public async Task Should_be_able_to_use_position_in_CoderrCollection()
+        {
+            var repos = Substitute.For();
+            var incident = new IncidentSummaryDTO(1, "Hello");
+            var data = new Dictionary {{"Longitude", "60.6065"}, {"Latitude", "15.6355"}};
+            var report = new ReportDTO {ContextCollections = new[] {new ContextCollectionDTO("CoderrData", data)}};
+            var e = new ReportAddedToIncident(incident, report, false);
+            var context = Substitute.For();
+            var configWrapper = Substitute.For>();
+
+            var sut = new StoreLocationFromNewReport(repos, configWrapper);
+            await sut.HandleAsync(context, e);
+
+            var entity = (ErrorOrigin)repos.ReceivedCalls().First(x => x.GetMethodInfo().Name == "CreateAsync")
+                .GetArguments().First();
+            entity.Latitude.Should().Be(15.6355);
+            entity.Longitude.Should().Be(60.6065);
+        }
+
+        [Fact]
+        public async Task Should_be_able_to_use_position_in_regular_collection()
+        {
+            var repos = Substitute.For();
+            var incident = new IncidentSummaryDTO(1, "Hello");
+            var data = new Dictionary {{"ReportLongitude", "60.6065"}, {"ReportLatitude", "15.6355"}};
+            var report = new ReportDTO {ContextCollections = new[] {new ContextCollectionDTO("SomeCollection", data)}};
+            var e = new ReportAddedToIncident(incident, report, false);
+            var context = Substitute.For();
+            var configWrapper = Substitute.For>();
+
+            var sut = new StoreLocationFromNewReport(repos, configWrapper);
+            await sut.HandleAsync(context, e);
+
+            var entity = (ErrorOrigin)repos.ReceivedCalls().First(x => x.GetMethodInfo().Name == "CreateAsync")
+                .GetArguments().First();
+            entity.Latitude.Should().Be(15.6355);
+            entity.Longitude.Should().Be(60.6065);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorReports/HashCodeGenerators/HashCodeGeneratorTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorReports/HashCodeGenerators/HashCodeGeneratorTests.cs
new file mode 100644
index 00000000..916c13e5
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorReports/HashCodeGenerators/HashCodeGeneratorTests.cs
@@ -0,0 +1,71 @@
+using System;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.ErrorReports;
+using FluentAssertions;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.ErrorReports.HashCodeGenerators
+{
+    public class HashCodeGeneratorTests
+    {
+
+        [Fact]
+        public void Should_remove_AT_from_stacktrace_to_avoid_multi_language_issues()
+        {
+            var exceptionWithAt = new ErrorReportException()
+            {
+                StackTrace =
+                    @"at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
+   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
+   at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
+   at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
+   at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)
+   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite, String methodName)
+   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
+   at Coderr.Server.Infrastructure.Configuration.Database.DatabaseStore.Store(IConfigurationSection section)
+   at Coderr.Server.Premise.App.LicenseWrapper.IncreaseReportCount()
+   at Coderr.Server.Web.Controllers.ReportReceiverController.PremiseLicenseCheck()
+   at Coderr.Server.Web.Controllers.ReportReceiverController.Post(String appKey, String sig)
+   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
+   at System.Threading.Tasks.ValueTask`1.get_Result()
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync():55
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
+   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync():33"
+            };
+            var entityWithAt = new ErrorReportEntity(1, "flldfd", DateTime.UtcNow, exceptionWithAt, new ErrorReportContextCollection[0]);
+            var exceptionWithoutAt = new ErrorReportException()
+            {
+                StackTrace =
+                    @"System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
+   System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
+   System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
+   System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
+   System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)
+   System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite, String methodName)
+   System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
+   Coderr.Server.Infrastructure.Configuration.Database.DatabaseStore.Store(IConfigurationSection section)
+   Coderr.Server.Premise.App.LicenseWrapper.IncreaseReportCount()
+   Coderr.Server.Web.Controllers.ReportReceiverController.PremiseLicenseCheck()
+   Coderr.Server.Web.Controllers.ReportReceiverController.Post(String appKey, String sig)
+   Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
+   System.Threading.Tasks.ValueTask`1.get_Result()
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync():55
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
+   Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync():33"
+            };
+            var entityWithoutAt = new ErrorReportEntity(1, "flldfd", DateTime.UtcNow, exceptionWithoutAt, new ErrorReportContextCollection[0]);
+            var sut = new HashCodeGenerator(new IHashCodeSubGenerator[0]);
+
+            var result1 = sut.GenerateHashCode(entityWithAt);
+            var result2 = sut.GenerateHashCode(entityWithAt);
+
+            result1.HashCode.Should().Be(result2.HashCode);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorReports/HashCodeGenerators/HttpErrorGeneratorTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorReports/HashCodeGenerators/HttpErrorGeneratorTests.cs
new file mode 100644
index 00000000..ce80f71a
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorReports/HashCodeGenerators/HttpErrorGeneratorTests.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.ErrorReports.HashcodeGenerators;
+using FluentAssertions;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.ErrorReports.HashCodeGenerators
+{
+    public class HttpErrorGeneratorTests
+    {
+        [Fact]
+        public void Should_remove_host_from_url()
+        {
+            var report = new ErrorReportEntity(1, "kffk", DateTime.UtcNow, new ErrorReportException(), new[]
+            {
+                new ErrorReportContextCollection("Yadayada",
+                    new Dictionary
+                    {
+                        {"Url", "http://localhost/some/path?hada=yada"},
+                        {"HttpCode", "404" }
+                    })
+            });
+
+            var sut = new HttpErrorGenerator();
+            var code = sut.GenerateHashCode(report);
+
+            var source = "404;/some/path";
+            code.HashCode.Should().Be(HashCodeUtility.GetPersistentHashCode(source).ToString("X"));
+        }
+
+        [Fact]
+        public void Should_remove_queryString_from_invalid_url()
+        {
+            var report = new ErrorReportEntity(1, "kffk", DateTime.UtcNow, new ErrorReportException(), new[]
+            {
+                new ErrorReportContextCollection("Yadayada",
+                    new Dictionary
+                    {
+                        {"Url", "http//localhost/some/path?hada=yada"},
+                        {"HttpCode", "404" }
+                    })
+            });
+
+            var sut = new HttpErrorGenerator();
+            var code = sut.GenerateHashCode(report);
+
+            var source = "404;http//localhost/some/path";
+            code.HashCode.Should().Be(HashCodeUtility.GetPersistentHashCode(source).ToString("X"));
+        }
+
+        [Fact]
+        public void Should_generate_from_relative_uri()
+        {
+            var report = new ErrorReportEntity(1, "kffk", DateTime.UtcNow, new ErrorReportException(), new[]
+            {
+                new ErrorReportContextCollection("Yadayada",
+                    new Dictionary
+                    {
+                        {"Url", "/some/path?hada=yada"},
+                        {"HttpCode", "404" }
+                    })
+            });
+
+            var sut = new HttpErrorGenerator();
+            var code = sut.GenerateHashCode(report);
+
+            var source = $"404;/some/path";
+            code.HashCode.Should().Be(HashCodeUtility.GetPersistentHashCode(source).ToString("X"));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorsReports/HashCodeGenerators/HashCodeGeneratorTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorsReports/HashCodeGenerators/HashCodeGeneratorTests.cs
new file mode 100644
index 00000000..c5237bd4
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorsReports/HashCodeGenerators/HashCodeGeneratorTests.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.ErrorReports;
+using FluentAssertions;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.ErrorsReports.HashCodeGenerators
+{
+    public class HashCodeGeneratorTests
+    {
+
+        [Fact]
+        public void Should_remove_AT_from_stacktrace_to_avoid_multi_language_issues()
+        {
+            var exceptionWithAt = new ErrorReportException()
+            {
+                StackTrace =
+                    @"at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
+   at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
+   at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
+   at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
+   at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)
+   at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite, String methodName)
+   at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
+   at Coderr.Server.Infrastructure.Configuration.Database.DatabaseStore.Store(IConfigurationSection section)
+   at Coderr.Server.Premise.App.LicenseWrapper.IncreaseReportCount()
+   at Coderr.Server.Web.Controllers.ReportReceiverController.PremiseLicenseCheck()
+   at Coderr.Server.Web.Controllers.ReportReceiverController.Post(String appKey, String sig)
+   at Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
+   at System.Threading.Tasks.ValueTask`1.get_Result()
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync():55
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
+   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
+   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync():33"
+            };
+            var entityWithAt = new ErrorReportEntity(1, "flldfd", DateTime.UtcNow, exceptionWithAt, new ErrorReportContextCollection[0]);
+            var exceptionWithoutAt = new ErrorReportException()
+            {
+                StackTrace =
+                    @"System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
+   System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
+   System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
+   System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
+   System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)
+   System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite, String methodName)
+   System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
+   Coderr.Server.Infrastructure.Configuration.Database.DatabaseStore.Store(IConfigurationSection section)
+   Coderr.Server.Premise.App.LicenseWrapper.IncreaseReportCount()
+   Coderr.Server.Web.Controllers.ReportReceiverController.PremiseLicenseCheck()
+   Coderr.Server.Web.Controllers.ReportReceiverController.Post(String appKey, String sig)
+   Microsoft.AspNetCore.Mvc.Internal.ActionMethodExecutor.TaskOfIActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
+   System.Threading.Tasks.ValueTask`1.get_Result()
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeActionMethodAsync():55
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeNextActionFilterAsync()
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
+   Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
+   Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextExceptionFilterAsync():33"
+            };
+            var entityWithoutAt = new ErrorReportEntity(1, "flldfd", DateTime.UtcNow, exceptionWithoutAt, new ErrorReportContextCollection[0]);
+            var sut = new HashCodeGenerator(new IHashCodeSubGenerator[0]);
+
+            var result1 = sut.GenerateHashCode(entityWithAt);
+            var result2 = sut.GenerateHashCode(entityWithAt);
+
+            result1.HashCode.Should().Be(result2.HashCode);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorsReports/HashCodeGenerators/HttpErrorGeneratorTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorsReports/HashCodeGenerators/HttpErrorGeneratorTests.cs
new file mode 100644
index 00000000..e9e9e177
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/ErrorsReports/HashCodeGenerators/HttpErrorGeneratorTests.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.ErrorReports.HashcodeGenerators;
+using FluentAssertions;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.ErrorsReports.HashCodeGenerators
+{
+    public class HttpErrorGeneratorTests
+    {
+        [Fact]
+        public void Should_remove_host_from_url()
+        {
+            var report = new ErrorReportEntity(1, "kffk", DateTime.UtcNow, new ErrorReportException(), new[]
+            {
+                new ErrorReportContextCollection("Yadayada",
+                    new Dictionary
+                    {
+                        {"Url", "http://localhost/some/path?hada=yada"},
+                        {"HttpCode", "404" }
+                    })
+            });
+
+            var sut = new HttpErrorGenerator();
+            var code = sut.GenerateHashCode(report);
+
+            var source = "404;/some/path";
+            code.HashCode.Should().Be(HashCodeUtility.GetPersistentHashCode(source).ToString("X"));
+        }
+
+        [Fact]
+        public void Should_remove_host_from_invalid_url()
+        {
+            var report = new ErrorReportEntity(1, "kffk", DateTime.UtcNow, new ErrorReportException(), new[]
+            {
+                new ErrorReportContextCollection("Yadayada",
+                    new Dictionary
+                    {
+                        {"Url", "http//localhost/some/path?hada=yada"},
+                        {"HttpCode", "404" }
+                    })
+            });
+
+            var sut = new HttpErrorGenerator();
+            var code = sut.GenerateHashCode(report);
+
+            var source = "404;http//localhost/some/path";
+            code.HashCode.Should().Be(HashCodeUtility.GetPersistentHashCode(source).ToString("X"));
+        }
+
+        [Fact]
+        public void Should_generate_from_relative_uri()
+        {
+            var report = new ErrorReportEntity(1, "kffk", DateTime.UtcNow, new ErrorReportException(), new[]
+            {
+                new ErrorReportContextCollection("Yadayada",
+                    new Dictionary
+                    {
+                        {"Url", "/some/path?hada=yada"},
+                        {"HttpCode", "404" }
+                    })
+            });
+
+            var sut = new HttpErrorGenerator();
+            var code = sut.GenerateHashCode(report);
+
+            var source = $"404;/some/path";
+            code.HashCode.Should().Be(HashCodeUtility.GetPersistentHashCode(source).ToString("X"));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/Similarities/Handlers/Processing/WmiDateConverterTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/Similarities/Handlers/Processing/WmiDateConverterTests.cs
new file mode 100644
index 00000000..b2784d9a
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/Similarities/Handlers/Processing/WmiDateConverterTests.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Text;
+using Coderr.Server.ReportAnalyzer.Similarities.Handlers.Processing;
+using FluentAssertions;
+using FluentAssertions.Extensions;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.Similarities.Handlers.Processing
+{
+    public class WmiDateConverterTests
+    {
+        [Fact]
+        public void Test1()
+        {
+            //19th minus 8 hours for the timezone
+            var expected = new DateTime(2009, 02, 18, 16, 0, 0);
+
+            var success = WmiDateConverter.TryParse("20090219000000.000000+480", out var result);
+
+            success.Should().BeTrue();
+            result.Should().Be(expected);
+        }
+
+        [Fact]
+        public void Test2()
+        {
+            var expected = new DateTime(2014, 04, 08, 15, 18, 35);
+            expected = expected.AddMicroseconds(999999);
+
+            var success = WmiDateConverter.TryParse("20140408141835.999999-60", out var result);
+
+            success.Should().BeTrue();
+            result.Should().Be(expected);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/Tagging/IdentifyTagsFromIncidentTests.cs b/src/Server/Coderr.Server.ReportAnalyzer.Tests/Tagging/IdentifyTagsFromIncidentTests.cs
new file mode 100644
index 00000000..baaf4e4d
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/Tagging/IdentifyTagsFromIncidentTests.cs
@@ -0,0 +1,92 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Modules.Tags;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using Coderr.Server.ReportAnalyzer.Tagging;
+using Coderr.Server.ReportAnalyzer.Tagging.Handlers;
+using DotNetCqs;
+using FluentAssertions;
+using NSubstitute;
+using Xunit;
+
+namespace Coderr.Server.ReportAnalyzer.Tests.Tagging
+{
+    public class IdentifyTagsFromIncidentTests
+    {
+        [Fact]
+        public async Task should_be_able_to_identity_tags_when_only_one_is_specified()
+        {
+            var repos = Substitute.For();
+            var provider = Substitute.For();
+            provider.GetIdentifiers(Arg.Any()).Returns(new ITagIdentifier[0]);
+            var ctx = Substitute.For();
+            var incident = new IncidentSummaryDTO(1, "Ada");
+            var report = new ReportDTO
+            {
+                ContextCollections = new[]
+                    {new ContextCollectionDTO("Data", new Dictionary {{"ErrTags", "MyTag"}})}
+            };
+            var e = new ReportAddedToIncident(incident, report, false);
+
+            var sut = new IdentifyTagsFromIncident(repos, provider);
+            await sut.HandleAsync(ctx, e);
+
+            var arguments = repos.ReceivedCalls().First(x => x.GetMethodInfo().Name == "AddAsync")
+                .GetArguments();
+            var tags = (Tag[]) arguments[1];
+            tags[0].Name.Should().Be("MyTag");
+        }
+
+        [Fact]
+        public async Task should_be_able_to_identity_tags_when_only_multiple_tags_are_specified()
+        {
+            var repos = Substitute.For();
+            var provider = Substitute.For();
+            provider.GetIdentifiers(Arg.Any()).Returns(new ITagIdentifier[0]);
+            var ctx = Substitute.For();
+            var incident = new IncidentSummaryDTO(1, "Ada");
+            var report = new ReportDTO
+            {
+                ContextCollections = new[]
+                    {new ContextCollectionDTO("Data", new Dictionary {{"ErrTags", "MyTag,YourTag"}})}
+            };
+            var e = new ReportAddedToIncident(incident, report, false);
+
+            var sut = new IdentifyTagsFromIncident(repos, provider);
+            await sut.HandleAsync(ctx, e);
+
+            var arguments = repos.ReceivedCalls().First(x => x.GetMethodInfo().Name == "AddAsync")
+                .GetArguments();
+            var tags = (Tag[]) arguments[1];
+            tags[0].Name.Should().Be("MyTag");
+            tags[1].Name.Should().Be("YourTag");
+        }
+
+        [Fact]
+        public async Task should_be_able_to_identity_tags_when_only_multiple_tags_are_specified_with_spaces()
+        {
+            var repos = Substitute.For();
+            var provider = Substitute.For();
+            provider.GetIdentifiers(Arg.Any()).Returns(new ITagIdentifier[0]);
+            var ctx = Substitute.For();
+            var incident = new IncidentSummaryDTO(1, "Ada");
+            var report = new ReportDTO
+            {
+                ContextCollections = new[]
+                    {new ContextCollectionDTO("Data", new Dictionary {{"ErrTags[]", "MyTag"}, {"ErrTags[]", "YourTag"}})}
+            };
+            var e = new ReportAddedToIncident(incident, report, false);
+
+            var sut = new IdentifyTagsFromIncident(repos, provider);
+            await sut.HandleAsync(ctx, e);
+
+            var arguments = repos.ReceivedCalls().First(x => x.GetMethodInfo().Name == "AddAsync")
+                .GetArguments();
+            var tags = (Tag[]) arguments[1];
+            tags[0].Name.Should().Be("MyTag");
+            tags[1].Name.Should().Be("YourTag");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer.Tests/codeRR.Server.ReportAnalyzer.Tests.csproj b/src/Server/Coderr.Server.ReportAnalyzer.Tests/codeRR.Server.ReportAnalyzer.Tests.csproj
new file mode 100644
index 00000000..abcb060d
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer.Tests/codeRR.Server.ReportAnalyzer.Tests.csproj
@@ -0,0 +1,28 @@
+
+  
+    netcoreapp3.1
+    true
+    true
+  
+
+  
+    
+    
+    
+      all
+      runtime; build; native; contentfiles; analyzers; buildtransitive
+    
+    
+    
+  
+
+  
+    
+    
+  
+
+  
+    
+  
+
+
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/AnalysisUnitOfWork.cs b/src/Server/Coderr.Server.ReportAnalyzer/AnalysisUnitOfWork.cs
new file mode 100644
index 00000000..c7c0fd47
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/AnalysisUnitOfWork.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Data;
+using System.Data.Common;
+using Coderr.Server.Abstractions;
+using Griffin;
+using Griffin.Data;
+
+namespace Coderr.Server.ReportAnalyzer
+{
+    public class AnalysisUnitOfWork : IAdoNetUnitOfWork, IGotTransaction
+    {
+        private readonly bool _ownsConnection;
+        private IDbConnection _connection;
+
+        public AnalysisUnitOfWork(IDbConnection connection, bool ownsConnection)
+        {
+            _connection = connection ?? throw new ArgumentNullException(nameof(connection));
+            _ownsConnection = ownsConnection;
+            Transaction = (DbTransaction)connection.BeginTransaction();
+        }
+
+        public DbTransaction Transaction { get; private set; }
+
+        public void Dispose()
+        {
+            if (Transaction != null)
+            {
+                Transaction.Dispose();
+                Transaction = null;
+            }
+
+            if (_connection != null && _ownsConnection)
+            {
+                _connection.Dispose();
+                _connection = null;
+            }
+        }
+
+        public void SaveChanges()
+        {
+            if (Transaction == null)
+                return;
+
+            Transaction.Commit();
+            Transaction.Dispose();
+            Transaction = null;
+        }
+
+        public IDbCommand CreateCommand()
+        {
+            var cmd = _connection.CreateCommand();
+            cmd.Transaction = Transaction;
+            return cmd;
+        }
+
+        /// 
+        ///     Execute a SQL query within the transaction
+        /// 
+        /// 
+        /// 
+        public void Execute(string sql, object parameters)
+        {
+            using (var cmd = CreateCommand())
+            {
+                cmd.CommandText = sql;
+                var dictionary = parameters.ToDictionary();
+                foreach (var kvp in dictionary) cmd.AddParameter(kvp.Key, kvp.Value);
+                cmd.ExecuteNonQuery();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ApplicationVersions/Handlers/GetVersionFromReport.cs b/src/Server/Coderr.Server.ReportAnalyzer/ApplicationVersions/Handlers/GetVersionFromReport.cs
new file mode 100644
index 00000000..b16c8ded
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ApplicationVersions/Handlers/GetVersionFromReport.cs
@@ -0,0 +1,128 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Client;
+using Coderr.Server.Domain.Modules.ApplicationVersions;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.ApplicationVersions.Handlers
+{
+    internal class GetVersionFromReport : IMessageHandler
+    {
+        public const string AppAssemblyVersion = "AppAssemblyVersion";
+        private readonly IApplicationVersionRepository _repository;
+
+        public GetVersionFromReport(IApplicationVersionRepository repository)
+        {
+            _repository = repository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            string version = null;
+            foreach (var contextCollection in e.Report.ContextCollections)
+            {
+                if (contextCollection.Properties.TryGetValue(AppAssemblyVersion, out version))
+                    break;
+            }
+
+            if (version == null)
+            {
+                return;
+            }
+
+            version = CleanVersionFromUnwantedCharacters(version);
+            version = SimplifyVersion(version);
+
+            if (version.Length > 20)
+            {
+                Err.ReportLogicError("Application version is too large.", new { version, e.Incident.ApplicationName },
+                    "AppVersionLength");
+                return;
+            }
+
+            var isNewIncident = e.Incident.ReportCount <= 1;
+            var versionEntity = await _repository.FindVersionAsync(e.Incident.ApplicationId, version)
+                                ?? new ApplicationVersion(e.Incident.ApplicationId, e.Incident.ApplicationName,
+                                    version);
+            if (versionEntity.Version != version)
+            {
+                versionEntity = new ApplicationVersion(e.Incident.ApplicationId, e.Incident.ApplicationName, version);
+            }
+
+            versionEntity.UpdateReportDate();
+
+            if (versionEntity.Id == 0)
+            {
+                await _repository.CreateAsync(versionEntity);
+            }
+            else
+            {
+                await _repository.UpdateAsync(versionEntity);
+            }
+
+
+            _repository.SaveIncidentVersion(e.Incident.Id, versionEntity.Id);
+
+            await IncreaseReportCounter(versionEntity.Id, isNewIncident, e.Report.CreatedAtUtc);
+        }
+
+        /// 
+        /// Remove .0 in the end.
+        /// 
+        /// 
+        /// 
+        /// 
+        public static string SimplifyVersion(string version)
+        {
+            var parts = version.Split('.');
+            var index = 0;
+            for (; index < parts.Length; index++)
+            {
+                if (parts[index] == "0")
+                {
+                    break;
+                }
+            }
+
+            if (index == 1)
+            {
+                return $"{parts[0]}.0";
+            }
+
+            return string.Join(".", parts.Take(index));
+        }
+
+        private static string CleanVersionFromUnwantedCharacters(string version)
+        {
+            var tmp = "";
+            foreach (var ch in version)
+            {
+                if (char.IsDigit(ch) || ch == '.')
+                    tmp += ch;
+            }
+
+            version = tmp;
+            return version;
+        }
+
+        private async Task IncreaseReportCounter(int versionId, bool isNewIncident, DateTime reportedAtUtc)
+        {
+            var month =
+                await _repository.FindMonthForApplicationAsync(versionId, reportedAtUtc.Year, reportedAtUtc.Month) ??
+                new ApplicationVersionMonth(versionId, new DateTime(reportedAtUtc.Year, reportedAtUtc.Month, 1));
+
+            if (isNewIncident)
+                month.IncreaseIncidentCount();
+            else
+                month.IncreaseReportCount();
+
+            if (month.Id == 0)
+                await _repository.CreateAsync(month);
+
+            else
+                await _repository.UpdateAsync(month);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ConfigWrapper.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ConfigWrapper.cs
new file mode 100644
index 00000000..fa656054
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ConfigWrapper.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using Coderr.Server.Abstractions.Config;
+
+namespace Coderr.Server.ReportAnalyzer.Boot.Adapters
+{
+    public class ConfigWrapper : IConfiguration
+        where TConfigType : IConfigurationSection, new()
+    {
+        private readonly ConfigurationStore _configurationStore;
+        private TConfigType _value;
+
+        public ConfigWrapper(ConfigurationStore configurationStore)
+        {
+            _configurationStore = configurationStore;
+        }
+
+        public TConfigType Value
+        {
+            get
+            {
+                if (EqualityComparer.Default.Equals(_value, default(TConfigType)))
+                    _value = _configurationStore.Load();
+
+                return _value;
+            }
+        }
+
+        public void Save()
+        {
+            _configurationStore.Store(Value);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/DomainQueueWrapper.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/DomainQueueWrapper.cs
new file mode 100644
index 00000000..dcb9030b
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/DomainQueueWrapper.cs
@@ -0,0 +1,35 @@
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using DotNetCqs.Bus;
+
+namespace Coderr.Server.ReportAnalyzer.Boot.Adapters
+{
+    /// 
+    ///     Publishes events in the other bounded context (the application context)
+    /// 
+    internal class DomainQueueWrapper3 : IDomainQueue
+    {
+        private readonly ScopedMessageBus _messageBus;
+        private bool _gotMessages;
+
+        public DomainQueueWrapper3(ScopedMessageBus messageBus)
+        {
+            _messageBus = messageBus;
+        }
+
+        public async Task PublishAsync(ClaimsPrincipal principal, object message)
+        {
+            await _messageBus.SendAsync(principal, message);
+            _gotMessages = true;
+        }
+
+
+        public async Task SaveChanges()
+        {
+            if (_gotMessages)
+                await _messageBus.CommitAsync();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/MessagingPrincipalWrapper.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/MessagingPrincipalWrapper.cs
new file mode 100644
index 00000000..7bfca3eb
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/MessagingPrincipalWrapper.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Security.Claims;
+using Coderr.Server.Abstractions.Security;
+using DotNetCqs.Logging;
+using log4net;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Coderr.Server.ReportAnalyzer.Boot.Adapters
+{
+    /// 
+    ///     This principal should always be specified by the queue listeners when a new message is received.
+    /// 
+    internal class MessagingPrincipalWrapper : IPrincipalAccessor
+    {
+        private ILog _logger = LogManager.GetLogger(typeof(MessagingPrincipalWrapper));
+
+        public MessagingPrincipalWrapper()
+        {
+        }
+
+        public ClaimsPrincipal Principal { get; set; }
+
+        public ClaimsPrincipal FindPrincipal()
+        {
+            return Principal;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ScopeWrapper.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ScopeWrapper.cs
new file mode 100644
index 00000000..dc2f2b30
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ScopeWrapper.cs
@@ -0,0 +1,30 @@
+using System;
+using DotNetCqs.DependencyInjection;
+using DotNetCqs.DependencyInjection.Microsoft;
+
+namespace Coderr.Server.ReportAnalyzer.Boot.Adapters
+{
+    internal class ScopeWrapper : IHandlerScopeFactory
+    {
+        private readonly Func _serviceProviderAccessor;
+        private MicrosoftHandlerScopeFactory _scopeFactory;
+
+        public ScopeWrapper(Func serviceProviderAccessor)
+        {
+            _serviceProviderAccessor = serviceProviderAccessor;
+        }
+
+        public IHandlerScope CreateScope()
+        {
+            if (_scopeFactory == null)
+            {
+                var provider = _serviceProviderAccessor();
+                if (provider == null)
+                    throw new InvalidOperationException("container have not been setup properly yet.");
+                _scopeFactory = new MicrosoftHandlerScopeFactory(provider);
+            }
+
+            return _scopeFactory.CreateScope();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ServerConfigSectionWrapper.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ServerConfigSectionWrapper.cs
new file mode 100644
index 00000000..b8c2426f
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ServerConfigSectionWrapper.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+using System.Linq;
+using Coderr.Server.ReportAnalyzer.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Boot.Adapters
+{
+    public class ServerConfigSectionWrapper : IConfigurationSection
+    {
+        private readonly Server.Abstractions.Boot.IConfigurationSection _inner;
+
+        public ServerConfigSectionWrapper(Server.Abstractions.Boot.IConfigurationSection inner)
+        {
+            _inner = inner;
+        }
+
+        public string this[string name] => _inner[name];
+
+        public IEnumerable GetChildren()
+        {
+            return _inner.GetChildren().Select(x => new ServerConfigSectionWrapper(x));
+        }
+
+        public string Value => _inner.Value;
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ServerConfigWrapper.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ServerConfigWrapper.cs
new file mode 100644
index 00000000..d3e01fd3
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Adapters/ServerConfigWrapper.cs
@@ -0,0 +1,33 @@
+using System.Collections.Generic;
+using System.Linq;
+using Coderr.Server.ReportAnalyzer.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Boot.Adapters
+{
+    public class ServerConfigWrapper : IConfiguration
+    {
+        private readonly Server.Abstractions.Boot.IConfiguration _inner;
+
+        public ServerConfigWrapper(Server.Abstractions.Boot.IConfiguration inner)
+        {
+            _inner = inner;
+        }
+
+        public string this[string name] => _inner[name];
+
+        public IEnumerable GetChildren()
+        {
+            return _inner.GetChildren().Select(x => new ServerConfigSectionWrapper(x));
+        }
+
+        public IConfigurationSection GetSection(string name)
+        {
+            return new ServerConfigSectionWrapper(_inner.GetSection(name));
+        }
+
+        public string GetConnectionString(string name)
+        {
+            return _inner.GetConnectionString(name);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/IAnalysisModule.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/IAnalysisModule.cs
new file mode 100644
index 00000000..5d7ae3b8
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/IAnalysisModule.cs
@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Coderr.Server.ReportAnalyzer.Boot
+{
+    public interface IAnalysisModule
+    {
+
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/RegisterExtensions.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/RegisterExtensions.cs
new file mode 100644
index 00000000..acdaf0fa
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/RegisterExtensions.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Reflection;
+using Coderr.Server.Abstractions.Boot;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Coderr.Server.ReportAnalyzer.Boot
+{
+    public static class RegisterExtensions
+    {
+        public static void RegisterContainerServices(this IServiceCollection serviceCollection, Assembly assembly)
+        {
+            var containerServices = assembly.GetTypes()
+                .Where(x => x.GetCustomAttribute(true) != null)
+                .ToList();
+            foreach (var containerService in containerServices)
+            {
+                var attr = containerService.GetCustomAttribute();
+                var interfaces = containerService.GetInterfaces();
+                var lifetime = ConvertLifetime(attr);
+
+                // Hack so that the same instance is resolved for each interface
+                var isRegisteredAsSelf = false;
+                if (interfaces.Length > 1 || attr.RegisterAsSelf)
+                {
+                    serviceCollection.Add(new ServiceDescriptor(containerService, containerService, lifetime));
+                    isRegisteredAsSelf = true;
+                }
+
+                foreach (var @interface in interfaces)
+                {
+                    var sd = isRegisteredAsSelf
+                        ? new ServiceDescriptor(@interface, x => x.GetService(containerService), lifetime) // else we don't get the same instance in the scope.
+                        : new ServiceDescriptor(@interface, containerService, lifetime);
+                    serviceCollection.Add(sd);
+                }
+            }
+        }
+
+        public static void RegisterMessageHandlers(this IServiceCollection serviceCollection, Assembly assembly)
+        {
+            var types = assembly.GetTypes()
+                .Where(y => y.GetInterfaces().Any(x => x.Name.Contains("IMessageHandler")))
+                .ToList();
+
+            foreach (var type in types)
+            {
+                if (type.GetCustomAttributes().Any(x => x.GetType().Name.StartsWith("ContainerService")))
+                {
+                    Debugger.Break();
+                }
+
+                serviceCollection.AddScoped(type, type);
+                serviceCollection.AddScoped(type.GetInterfaces()[0], x => x.GetService(type));
+            }
+        }
+
+
+
+        private static ServiceLifetime ConvertLifetime(ContainerServiceAttribute attr)
+        {
+            if (attr.IsSingleInstance)
+            {
+                return ServiceLifetime.Singleton;
+            }
+
+            if (attr.IsTransient)
+            {
+                return ServiceLifetime.Transient;
+            }
+
+            return ServiceLifetime.Scoped;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/ReportAnalyzerBootstrapper.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/ReportAnalyzerBootstrapper.cs
new file mode 100644
index 00000000..8e27be50
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/ReportAnalyzerBootstrapper.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Data;
+using System.Linq;
+using Coderr.Server.Abstractions;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Abstractions.Reports;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Infrastructure.Configuration.Database;
+using Coderr.Server.ReportAnalyzer.Boot.Adapters;
+using Griffin.Data;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Coderr.Server.ReportAnalyzer.Boot
+{
+    public class ReportAnalyzerBootstrapper : IAppModule
+    {
+        private readonly ReportAnalyzerModuleStarter _reportAnalyzerModuleStarter;
+        private ServiceProvider _serviceProvider;
+
+        public ReportAnalyzerBootstrapper()
+        {
+            _reportAnalyzerModuleStarter = new ReportAnalyzerModuleStarter();
+        }
+
+        public void Configure(ConfigurationContext context)
+        {
+            var serviceCollection = new ServiceCollection();
+
+            serviceCollection.AddSingleton(context.Configuration);
+
+            var ourContext = new Abstractions.Boot.ConfigurationContext(serviceCollection, GetServiceProvider)
+            {
+                Configuration = new ServerConfigWrapper(context.Configuration),
+                ConnectionFactory = context.ConnectionFactory,
+            };
+            var ignoredModules = context.Configuration.GetSection("DisabledModules:ReportAnalyzer");
+            _reportAnalyzerModuleStarter.Configure(ourContext, new ServerConfigSectionWrapper(ignoredModules));
+
+            if (!ServerConfig.Instance.IsLive)
+            {
+                ourContext.Services.AddScoped(typeof(IConfiguration<>), typeof(ConfigWrapper<>));
+                ourContext.Services.AddSingleton(x =>
+                    new DatabaseStore(() => context.ConnectionFactory(CoderrClaims.SystemPrincipal)));
+            }
+
+            ourContext.Services.AddScoped();
+            ourContext.Services.AddScoped(x =>
+                new AnalysisUnitOfWork(x.GetService(), false));
+            ourContext.Services.AddScoped(provider =>
+            {
+                var principal = provider.GetService().Principal
+                                ?? CoderrClaims.SystemPrincipal;
+
+                return context.ConnectionFactory(principal);
+            });
+
+            _serviceProvider = serviceCollection.BuildServiceProvider();
+            var collection = context.Services.Where(x => x.ServiceType.Name.Contains("IConfiguration")).ToList();
+            var b = _serviceProvider.GetService();
+            var d = _serviceProvider.GetRequiredService(typeof(IConfiguration<>).MakeGenericType(typeof(ReportConfig)));
+        }
+
+
+        public void Start(StartContext context)
+        {
+            var ourStartContext = new Abstractions.Boot.StartContext
+            {
+                ServiceProvider = _serviceProvider
+            };
+            _reportAnalyzerModuleStarter.Start(ourStartContext);
+        }
+
+
+        public void Stop()
+        {
+            _reportAnalyzerModuleStarter.Stop();
+        }
+
+        private IServiceProvider GetServiceProvider()
+        {
+            if (_serviceProvider == null)
+                throw new InvalidOperationException("service provider have not been built yet.");
+            return _serviceProvider;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/ReportAnalyzerModuleStarter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/ReportAnalyzerModuleStarter.cs
new file mode 100644
index 00000000..f38d43a4
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/ReportAnalyzerModuleStarter.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Coderr.Server.Abstractions;
+using Coderr.Server.ReportAnalyzer.Abstractions.Boot;
+using log4net;
+using Microsoft.Extensions.DependencyInjection;
+using ConfigurationContext = Coderr.Server.ReportAnalyzer.Abstractions.Boot.ConfigurationContext;
+using IConfigurationSection = Coderr.Server.ReportAnalyzer.Abstractions.Boot.IConfigurationSection;
+using StartContext = Coderr.Server.ReportAnalyzer.Abstractions.Boot.StartContext;
+
+namespace Coderr.Server.ReportAnalyzer.Boot
+{
+    public class ReportAnalyzerModuleStarter
+    {
+        private readonly List _ignoredModules = new List();
+        private readonly List _modules = new List();
+        private ILog _logger = LogManager.GetLogger(typeof(ReportAnalyzerModuleStarter));
+
+        public void Configure(ConfigurationContext context, IConfigurationSection ignoredModulesConfigurationSection)
+        {
+            foreach (var child in ignoredModulesConfigurationSection.GetChildren())
+                _ignoredModules.Add(child.Value);
+
+
+            ScanAssembliesForModules(AppDomain.CurrentDomain.BaseDirectory);
+
+            foreach (var module in _modules)
+            {
+                var childServices = new ServiceCollection();
+                var moduleContext = new ConfigurationContext(childServices, context.ServiceProvider)
+                {
+                    Configuration = context.Configuration,
+                    ConnectionFactory = context.ConnectionFactory
+                };
+
+                module.Configure(moduleContext);
+
+                foreach (var service in moduleContext.Services)
+                {
+                    var existing = context.Services.FirstOrDefault(x => x.ImplementationType == service.ImplementationType && x.ImplementationType != null);
+                    if (existing != null)
+                    {
+                        var allServices = childServices.OrderBy(x => x.ImplementationType?.Name).ToList();
+                        Debug.WriteLine("FAILLED " + existing.ServiceType + ": " + service.ImplementationType);
+                        throw new InvalidOperationException(
+                            $"Service {service.ImplementationType} has already been registered.");
+                    }
+
+                    _logger.Debug($"{module.GetType().Name} registers {service.ImplementationType?.ToString() ?? service.ServiceType + "[Service]"}");
+                    context.Services.Add(service);
+                }
+            }
+
+        }
+
+        public void Start(StartContext context)
+        {
+            foreach (var module in _modules) module.Start(context);
+        }
+
+        public void Stop()
+        {
+            foreach (var module in _modules) module.Stop();
+        }
+
+        private void RegisterModule(Type type)
+        {
+            var module = (IReportAnalyzerModule)Activator.CreateInstance(type);
+            _modules.Add(module);
+        }
+
+
+        private void ScanAssembliesForModules(string assemblyDirectory)
+        {
+            var files = Directory.GetFiles(assemblyDirectory, "*.dll");
+            foreach (var fullPath in files)
+            {
+                var fileName = Path.GetFileName(fullPath);
+                if (!fileName.Contains("Coderr"))
+                    continue;
+
+                var assembly = Assembly.LoadFrom(fullPath);
+                var types = assembly.GetTypes()
+                    .Where(x => typeof(IReportAnalyzerModule).IsAssignableFrom(x) && !x.IsAbstract && !x.IsInterface)
+                    .ToList();
+                foreach (var type in types)
+                {
+                    if (_ignoredModules.Any(x => x.Equals(type.Name, StringComparison.OrdinalIgnoreCase)))
+                        continue;
+                    if (ServerConfig.Instance.IsModuleIgnored(type))
+                    {
+                        continue;
+                    }
+
+                    RegisterModule(type);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/Starters/RegisterContainerServices.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Starters/RegisterContainerServices.cs
new file mode 100644
index 00000000..dbb1b8e5
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Starters/RegisterContainerServices.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using Coderr.Server.ReportAnalyzer.Abstractions.Boot;
+using Coderr.Server.ReportAnalyzer.Inbound.Handlers;
+
+namespace Coderr.Server.ReportAnalyzer.Boot.Starters
+{
+    public class RegisterContainerServices : IReportAnalyzerModule
+    {
+        public void Start(StartContext context)
+        {
+        }
+
+        public void Configure(ConfigurationContext context)
+        {
+            context.Services.RegisterContainerServices(Assembly.GetExecutingAssembly());
+
+            //workaround since SQL server already references us
+            var assembly = AppDomain.CurrentDomain.GetAssemblies()
+                .First(x => x.FullName.StartsWith("Coderr.Server.SqlServer,"));
+            context.Services.RegisterContainerServices(assembly);
+        }
+
+        public void Stop()
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Boot/Starters/ReportQueueModule.cs b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Starters/ReportQueueModule.cs
new file mode 100644
index 00000000..414871bc
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Boot/Starters/ReportQueueModule.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Linq;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Coderr.Client;
+using Coderr.Server.Abstractions;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Domain.Core.Incidents.Events;
+using Coderr.Server.Infrastructure.Messaging;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Coderr.Server.ReportAnalyzer.Abstractions.Boot;
+using Coderr.Server.ReportAnalyzer.Boot.Adapters;
+using DotNetCqs;
+using DotNetCqs.Bus;
+using DotNetCqs.DependencyInjection;
+using DotNetCqs.Logging;
+using DotNetCqs.MessageProcessor;
+using DotNetCqs.Queues;
+using Griffin.Data;
+using log4net;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Coderr.Server.ReportAnalyzer.Boot.Starters
+{
+    internal class ReportQueueModule : IReportAnalyzerModule
+    {
+        private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
+        private readonly ILog _logger = LogManager.GetLogger(typeof(ReportQueueModule));
+        private QueueListener _eventProcessor;
+        private QueueListener _reportListener;
+
+        public ReportQueueModule()
+        {
+        }
+
+        public void Configure(ConfigurationContext context)
+        {
+            _logger.Debug("configuring on  " + context.GetHashCode());
+            ConfigureListeners(context);
+            ConfigureMessageHandlers(context);
+
+            if (ServerConfig.Instance.IsLive)
+            {
+                return;
+            }
+
+            context.Services.AddSingleton(x =>
+            {
+                var queueName =
+                    ServerConfig.Instance.IsLive
+                        ? context.Configuration.GetSection("MessageQueue")["AppQueue"]
+                        : "Messaging";
+                var queue = QueueManager.Instance.QueueProvider.Open(queueName);
+                var bus = new SingleInstanceMessageBus(queue);
+                return bus;
+            });
+
+            CreateDomainQueue(context, "Messaging");
+        }
+
+        public void Start(StartContext context)
+        {
+            //These should not be blocking.But they are working as blocking. quite strange.
+
+
+            if (_reportListener != null)
+                ThreadPool.QueueUserWorkItem(state =>
+                {
+                    _reportListener.RunAsync(_cancellationTokenSource.Token).ContinueWith(HaveRun);
+                });
+
+            if (_eventProcessor != null)
+                ThreadPool.QueueUserWorkItem(state =>
+                {
+                    _eventProcessor.RunAsync(_cancellationTokenSource.Token).ContinueWith(HaveRun);
+                });
+        }
+
+        public void Stop()
+        {
+            _logger.Info("Report queue module is shutting down.");
+            _cancellationTokenSource.Cancel();
+        }
+
+        private void ConfigureListeners(ConfigurationContext context)
+        {
+            if (Environment.GetEnvironmentVariable("DisableReportQueue") == "1")
+                return;
+
+            var reportQueueName = ServerConfig.Instance.Queues.ReportQueue;
+            var reportEventQueue = ServerConfig.Instance.Queues.ReportEventQueue;
+            var appQueue = ServerConfig.Instance.Queues.AppQueue;
+
+            var typeName = ServerConfig.Instance.IsLive ? "LIVE" : "PREMISE";
+            _logger.Info($"Running AS  {typeName} with queues {reportQueueName}, {appQueue} and {reportEventQueue}");
+            _reportListener = ConfigureQueueListener(context, reportQueueName, reportEventQueue, appQueue);
+            _eventProcessor = ConfigureQueueListener(context, reportEventQueue, reportEventQueue, appQueue);
+        }
+
+        private void ConfigureMessageHandlers(ConfigurationContext context)
+        {
+            var assembly = Assembly.GetExecutingAssembly();
+            context.Services.RegisterMessageHandlers(assembly);
+
+            //workaround since SQL server already references us
+            assembly = AppDomain.CurrentDomain.GetAssemblies()
+                .First(x => x.FullName.StartsWith("Coderr.Server.SqlServer,"));
+            context.Services.RegisterMessageHandlers(assembly);
+        }
+
+        private QueueListener ConfigureQueueListener(ConfigurationContext context, string inboundQueueName,
+            string outboundQueueName, string appQueue)
+        {
+            var inboundQueue = QueueManager.Instance.GetQueue(inboundQueueName);
+            var outboundQueue = inboundQueueName == outboundQueueName
+                ? inboundQueue
+                : QueueManager.Instance.GetQueue(outboundQueueName);
+            var scopeFactory = new ScopeWrapper(context.ServiceProvider);
+
+            MessageRouter.Instance.RegisterReportQueue(outboundQueue);
+            MessageRouter.Instance.RegisterAppQueue(QueueManager.Instance.GetQueue(appQueue));
+
+            var listener = new QueueListener(inboundQueue, MessageRouter.Instance, scopeFactory)
+            {
+                RetryAttempts =
+                    new[] { TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2) },
+                MessageInvokerFactory = scope =>
+                {
+                    var invoker = new MessageInvoker(scope);
+                    invoker.InvokingHandler += (sender, args) =>
+                    {
+                        _logger.Debug("Invoking " + args.Handler.GetType().FullName);
+                    };
+                    invoker.HandlerMissing += (sender, args) =>
+                    {
+                        _logger.Error("Missing handler for " + args.Message.Body.GetType().FullName);
+                    };
+                    return invoker;
+                },
+            };
+            listener.PoisonMessageDetected += (sender, args) =>
+            {
+                Err.Report(args.Exception, new { args.Message.Body });
+                _logger.Error($"CoreReport [{inboundQueueName}] Poison message: {args.Message.Body}", args.Exception);
+            };
+            listener.ScopeCreated += (sender, args) =>
+            {
+                _logger.Debug($"CoreReport [{inboundQueueName} principal " + args.Principal.ToLogString());
+                var accessor = args.Scope.ResolveDependency().First();
+                accessor.Principal = args.Principal;
+            };
+            listener.ScopeClosing += (sender, args) =>
+            {
+                if (args.Exception != null)
+                    return;
+
+                var all = args.Scope.ResolveDependency().ToList();
+                all[0].SaveChanges();
+
+                var queue = (ISaveable)args.Scope.ResolveDependency().First();
+                queue.SaveChanges().GetAwaiter().GetResult();
+            };
+            listener.MessageInvokerFactory = MessageInvokerFactory;
+            return listener;
+        }
+
+        /// 
+        ///     Writes to the message queue that the application is processing (publishing in the other bounded context)
+        /// 
+        /// 
+        private void CreateDomainQueue(ConfigurationContext context, string queueName)
+        {
+            context.Services.AddScoped(x =>
+            {
+                var queue = QueueManager.Instance.GetQueue(queueName);
+                var messageBus = new ScopedMessageBus(queue);
+                return new DomainQueueWrapper3(messageBus);
+            });
+        }
+
+        private void HaveRun(Task obj)
+        {
+            _logger.Info("Stop completed for a listener in the ReportQueueModule. " + obj);
+        }
+
+        private IMessageInvoker MessageInvokerFactory(IHandlerScope arg)
+        {
+            var invoker = new MessageInvoker(arg);
+            invoker.HandlerMissing += (sender, args) =>
+            {
+                if (args.Message.Body is IncidentCreated)
+                    return;
+
+                _logger.Warn(
+                    "Failed to find a handler for " + args.Message.Body.GetType());
+            };
+            invoker.InvokingHandler += (sender, args) =>
+            {
+                var asseccor = arg.ResolveDependency().FirstOrDefault();
+                asseccor.Principal = args.Principal;
+            };
+
+            invoker.HandlerInvoked += (sender, args) =>
+            {
+                if (args.ExecutionTime.TotalMilliseconds > 500)
+                {
+
+                    _logger.Error(
+                        $"Ran {args.Handler}, took {args.ExecutionTime.TotalMilliseconds}ms. Credentials: {args.Principal.ToFriendlyString()}");
+                }
+                if (args.Exception == null)
+                    return;
+
+                //Err.Report(args.Exception, new
+                //{
+                //    args.Message,
+                //    HandlerType = args.Handler.GetType(),
+                //    args.ExecutionTime
+                //});
+                _logger.Error(
+                    $"Ran {args.Handler}, took {args.ExecutionTime.TotalMilliseconds}ms, but FAILED. Credentials: {args.Principal.ToFriendlyString()}",
+                    args.Exception);
+            };
+            return invoker;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Coderr.ReportAnalyzer.csproj.DotSettings b/src/Server/Coderr.Server.ReportAnalyzer/Coderr.ReportAnalyzer.csproj.DotSettings
new file mode 100644
index 00000000..c54c126d
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Coderr.ReportAnalyzer.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+	CSharp70
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj b/src/Server/Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj
new file mode 100644
index 00000000..9919d7bb
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj
@@ -0,0 +1,37 @@
+
+  
+    netstandard2.0
+    Coderr.Server.ReportAnalyzer
+    Coderr.Server.ReportAnalyzer
+    $(DefaultItemExcludes);**\*.DotSettings;
+    Debug;Release;Premise
+  
+  
+    
+    
+    
+  
+  
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+    
+  
+  
+    
+    
+    
+  
+  
+
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/Handlers/OriginsConfiguration.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/Handlers/OriginsConfiguration.cs
new file mode 100644
index 00000000..6ed88aa5
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/Handlers/OriginsConfiguration.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using Coderr.Server.Abstractions;
+using Coderr.Server.Abstractions.Config;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorOrigins.Handlers
+{
+    public class OriginsConfiguration : IConfigurationSection
+    {
+        /// 
+        ///     API key for ipstack
+        /// 
+        public string ApiKey { get; set; }
+
+        /// 
+        ///     API key for LocationIQ.com
+        /// 
+        public string LocationIqApiKey { get; set; }
+
+        /// 
+        ///     API key for mapquest.com
+        /// 
+        public string MapQuestApiKey { get; set; }
+
+        public bool IsConfigured =>
+            ServerConfig.Instance.ServerType != ServerType.Community
+            || !string.IsNullOrEmpty(ApiKey)
+            || !string.IsNullOrEmpty(LocationIqApiKey)
+            || !string.IsNullOrEmpty(MapQuestApiKey);
+         
+        string IConfigurationSection.SectionName { get; } = "Origins";
+
+        IDictionary IConfigurationSection.ToDictionary()
+        {
+            return this.ToConfigDictionary();
+        }
+
+
+        void IConfigurationSection.Load(IDictionary settings)
+        {
+            this.AssignProperties(settings);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/Handlers/StoreLocationFromNewReport.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/Handlers/StoreLocationFromNewReport.cs
new file mode 100644
index 00000000..fe75bf8e
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/Handlers/StoreLocationFromNewReport.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Globalization;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Domain.Modules.ErrorOrigins;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorOrigins.Handlers
+{
+    /// 
+    ///     Responsible of looking up geographic position of the IP address that delivered the report.
+    /// 
+    public class StoreLocationFromNewReport : IMessageHandler
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(StoreLocationFromNewReport));
+        private readonly IErrorOriginRepository _repository;
+        private readonly IConfiguration _originConfiguration;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repos
+        /// repository
+        public StoreLocationFromNewReport(IErrorOriginRepository repository,
+            IConfiguration originConfiguration)
+        {
+            _repository = repository ?? throw new ArgumentNullException(nameof(repository));
+            _originConfiguration = originConfiguration;
+        }
+
+        /// 
+        ///     Process an event asynchronously.
+        /// 
+        /// event to process
+        /// 
+        ///     Task to wait on.
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            _logger.Info("RemoteAddress: " + e.Report.RemoteAddress);
+
+            // Random swedish IP for testing purposes
+            if (e.Report.RemoteAddress == "::1" || e.Report.RemoteAddress == "127.0.0.1")
+                e.Report.RemoteAddress = "94.254.57.227";
+
+            if (e.IsStored != true || !_originConfiguration.Value.IsConfigured)
+            {
+                return;
+            }
+
+            var numberStyles = NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign;
+            var collection = e.Report.GetCoderrCollection();
+            if (collection != null)
+            {
+                var latitude = 0d;
+                var longitude = 0d;
+                var gotLat = collection.Properties.TryGetValue("Longitude", out var longitudeStr)
+                             && double.TryParse(longitudeStr, numberStyles,
+                                 CultureInfo.InvariantCulture, out longitude);
+
+                var gotLong = collection.Properties.TryGetValue("Latitude", out var latitudeStr)
+                              && double.TryParse(latitudeStr, numberStyles,
+                                  CultureInfo.InvariantCulture, out latitude);
+                if (gotLat && latitude > 0 && gotLong && longitude > 0)
+                {
+                    var errorOrigin2 = new ErrorOrigin(e.Report.RemoteAddress, longitude, latitude);
+                    await _repository.CreateAsync(errorOrigin2, e.Incident.ApplicationId, e.Incident.Id, e.Report.Id);
+                    return;
+                }
+            }
+
+
+            var latitude1 = e.Report.FindCollectionProperty("ReportLatitude");
+            var longitude1 = e.Report.FindCollectionProperty("ReportLongitude");
+            if (longitude1 != null
+                && double.TryParse(latitude1, numberStyles, CultureInfo.InvariantCulture,
+                    out var latitude2)
+                && latitude1 != null
+                && double.TryParse(longitude1, numberStyles, CultureInfo.InvariantCulture,
+                    out var longitude2))
+            {
+                var errorOrigin2 = new ErrorOrigin(e.Report.RemoteAddress, longitude2, latitude2);
+                await _repository.CreateAsync(errorOrigin2, e.Incident.ApplicationId, e.Incident.Id, e.Report.Id);
+                return;
+            }
+
+
+            if (string.IsNullOrEmpty(e.Report.RemoteAddress))
+            {
+                return;
+            }
+
+
+            var origin = new ErrorOrigin(e.Report.RemoteAddress);
+            await _repository.CreateAsync(origin, e.Incident.ApplicationId, e.Incident.Id, e.Report.Id);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/IErrorOriginRepository.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/IErrorOriginRepository.cs
new file mode 100644
index 00000000..c475ca30
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/IErrorOriginRepository.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Modules.ErrorOrigins;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorOrigins
+{
+    /// 
+    ///     Stores error origins
+    /// 
+    /// 
+    ///     TODO: Uses the IncidentTabl
+    /// 
+    public interface IErrorOriginRepository
+    {
+        /// 
+        ///     Create a new entry
+        /// 
+        /// origin
+        /// Application that we received a report for
+        /// incident that the report belongs to
+        /// report received that we got a location for
+        /// task
+        /// origin
+        Task CreateAsync(ErrorOrigin entity, int applicationId, int incidentId, int reportId);
+
+        Task> GetPendingOrigins();
+
+        Task Update(ErrorOrigin entity);
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/LookupInboundOrigins.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/LookupInboundOrigins.cs
new file mode 100644
index 00000000..0e22e1f9
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorOrigins/LookupInboundOrigins.cs
@@ -0,0 +1,394 @@
+using System;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Threading.Tasks;
+using Coderr.Client;
+using Coderr.Server.Abstractions;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Domain.Modules.ErrorOrigins;
+using Coderr.Server.ReportAnalyzer.ErrorOrigins.Handlers;
+using Griffin.ApplicationServices;
+using Newtonsoft.Json.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorOrigins
+{
+    [ContainerService(RegisterAsSelf = true)]
+    public class LookupInboundOrigins : IBackgroundJobAsync
+    {
+        private readonly IErrorOriginRepository _errorOriginRepository;
+        private readonly IConfiguration _originConfiguration;
+
+        public LookupInboundOrigins(IErrorOriginRepository errorOriginRepository, IConfiguration originConfiguration)
+        {
+            _errorOriginRepository = errorOriginRepository;
+            _originConfiguration = originConfiguration;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            var toLookup = await _errorOriginRepository.GetPendingOrigins();
+            foreach (var origin in toLookup)
+            {
+                if (origin.Longitude > 180)
+                {
+                    //await LookupIpAddress(origin);
+                    await CommercialLookup(origin);
+                    await _errorOriginRepository.Update(origin);
+                    continue;
+                }
+
+                if (!string.IsNullOrEmpty(_originConfiguration.Value.LocationIqApiKey))
+                {
+                    await LookupCoordinatesUsingLocationIq(_originConfiguration.Value.LocationIqApiKey, origin);
+                }
+                else if (!string.IsNullOrEmpty(_originConfiguration.Value.MapQuestApiKey))
+                {
+                    await LookupCoordinatesUsingMapQuest(_originConfiguration.Value.MapQuestApiKey, origin);
+                }
+
+                await _errorOriginRepository.Update(origin);
+            }
+        }
+
+        private async Task LookupIpAddress(ErrorOrigin origin)
+        {
+            if (string.IsNullOrEmpty(_originConfiguration.Value.ApiKey))
+                return;
+
+            if (!IPAddress.TryParse(origin.IpAddress, out var address))
+                return;
+
+            if (address.IsInternal())
+            {
+                return;
+            }
+
+            var url = $"http://api.ipstack.com/{origin.IpAddress}?access_key={_originConfiguration.Value.ApiKey}";
+            var request = WebRequest.CreateHttp(url);
+            var json = "";
+            try
+            {
+                var response = await request.GetResponseAsync();
+                var stream = response.GetResponseStream();
+                var reader = new StreamReader(stream);
+                json = await reader.ReadToEndAsync();
+                var jsonObj = JObject.Parse(json);
+
+                /*
+                    {
+                       "ip":"94.254.21.175",
+                       "country_code":"SE",
+                       "country_name":"Sweden",
+                       "region_code":"10",
+                       "region_name":"Dalarnas Lan",
+                       "city":"Falun",
+                       "zipcode":"",
+                       "latitude":60.6,
+                       "longitude":15.6333,
+                       "metro_code":"",
+                       "areacode":""
+                    }
+                 */
+
+                // Key is only included in error messages.
+                if (jsonObj.ContainsKey("success"))
+                {
+                    return;
+                }
+
+                var lat = double.Parse(jsonObj["latitude"].Value(), CultureInfo.InvariantCulture);
+                var lon = double.Parse(jsonObj["longitude"].Value(), CultureInfo.InvariantCulture);
+                origin.Longitude = lon;
+                origin.Latitude = lat;
+                origin.City = jsonObj["city"].ToString();
+                origin.CountryCode = jsonObj["country_code"].ToString();
+                origin.CountryName = jsonObj["country_name"].ToString();
+                origin.RegionCode = jsonObj["region_code"].ToString();
+                origin.RegionName = jsonObj["region_name"].ToString();
+                origin.ZipCode = jsonObj["zip"].ToString();
+            }
+            catch (Exception exception)
+            {
+                throw new InvalidOperationException($"Failed to call lookupService or parse the JSON: {json}.", exception);
+            }
+        }
+
+        private async Task LookupCoordinatesUsingMapQuest(string mapQuestKey, ErrorOrigin origin)
+        {
+            #region JSON example
+            /*
+                {
+                  "info": {
+                    "statuscode": 0,
+                    "copyright": {
+                      "text": "© 2018 MapQuest, Inc.",
+                      "imageUrl": "http://api.mqcdn.com/res/mqlogo.gif",
+                      "imageAltText": "© 2018 MapQuest, Inc."
+                    },
+                    "messages": []
+                  },
+                  "options": {
+                    "maxResults": 1,
+                    "thumbMaps": true,
+                    "ignoreLatLngInput": false
+                  },
+                  "results": [
+                    {
+                      "providedLocation": {
+                        "latLng": {
+                          "lat": 30.333472,
+                          "lng": -81.470448
+                        }
+                      },
+                      "locations": [
+                        {
+                          "street": "12714 Ashley Melisse Blvd",
+                          "adminArea6": "",
+                          "adminArea6Type": "Neighborhood",
+                          "adminArea5": "Jacksonville",
+                          "adminArea5Type": "City",
+                          "adminArea4": "Duval",
+                          "adminArea4Type": "County",
+                          "adminArea3": "FL",
+                          "adminArea3Type": "State",
+                          "adminArea1": "US",
+                          "adminArea1Type": "Country",
+                          "postalCode": "32225",
+                          "geocodeQualityCode": "L1AAA",
+                          "geocodeQuality": "ADDRESS",
+                          "dragPoint": false,
+                          "sideOfStreet": "R",
+                          "linkId": "0",
+                          "unknownInput": "",
+                          "type": "s",
+                          "latLng": {
+                            "lat": 30.33472,
+                            "lng": -81.470448
+                          },
+                          "displayLatLng": {
+                            "lat": 30.333472,
+                            "lng": -81.470448
+                          },
+                          "mapUrl": "http://open.mapquestapi.com/staticmap/v4/getmap?key=KEY&type=map&size=225,160&pois=purple-1,30.3334721,-81.4704483,0,0,|¢er=30.3334721,-81.4704483&zoom=15&rand=-553163060",
+                          "nearestIntersection": {
+                            "streetDisplayName": "Posey Cir",
+                            "distanceMeters": "851755.1608527573",
+                            "latLng": {
+                              "longitude": -87.523761,
+                              "latitude": 35.013434
+                            },
+                            "label": "Danley Rd & Posey Cir"
+                          },
+                          "roadMetadata": {
+                            "speedLimitUnits": "mph",
+                            "tollRoad": null,
+                            "speedLimit": 40
+                          }
+                        }
+                      ]
+                    }
+                  ]
+                }
+             */
+            #endregion
+
+            var url =
+                $"http://open.mapquestapi.com/geocoding/v1/reverse?key={mapQuestKey}&location={origin.Latitude.ToString(CultureInfo.InvariantCulture)},{origin.Longitude.ToString(CultureInfo.InvariantCulture)}&includeRoadMetadata=true&includeNearestIntersection=true";
+            var request = WebRequest.CreateHttp(url);
+            var json = "";
+            try
+            {
+                var response = await request.GetResponseAsync();
+                var stream = response.GetResponseStream();
+                var reader = new StreamReader(stream);
+                json = await reader.ReadToEndAsync();
+                var jsonObj = JObject.Parse(json);
+
+                var array = (JArray)jsonObj["results"][0]["locations"];
+                if (array.Count == 0)
+                    return;
+
+                var firstLocation = jsonObj["results"][0]["locations"][0];
+                origin.ZipCode = firstLocation["postalCode"].Value();
+
+                /*  "street": "12714 Ashley Melisse Blvd",
+                          "adminArea6": "",
+                          "adminArea6Type": "Neighborhood",
+                          "adminArea5": "Jacksonville",
+                          "adminArea5Type": "City",
+                          "adminArea4": "Duval",
+                          "adminArea4Type": "County",
+                          "adminArea3": "FL",
+                          "adminArea3Type": "State",
+                          "adminArea1": "US",
+                          "adminArea1Type": "Country",
+                          "postalCode": "32225",*/
+
+                for (var i = 1; i <= 6; i++)
+                {
+                    var token = firstLocation[$"adminArea{i}"];
+                    var value = token?.Value();
+                    if (value == null)
+                        continue;
+
+                    switch (firstLocation[$"adminArea{i}Type"].Value())
+                    {
+                        case "Country":
+                            origin.CountryCode = value;
+                            break;
+                        case "State":
+                            origin.RegionName = value;
+                            break;
+                        case "County":
+                            break;
+                        case "City":
+                            origin.City = value;
+                            break;
+                    }
+                }
+
+                //origin.CountryName = firstLocation["country_name"].ToString();
+                //origin.RegionCode = firstLocation["region_code"].ToString();
+            }
+            catch (Exception exception)
+            {
+                throw new InvalidOperationException($"Failed to call lookupService or parse the JSON: {json}.", exception);
+            }
+        }
+
+        private async Task LookupCoordinatesUsingLocationIq(string apiKey, ErrorOrigin origin)
+        {
+            /*
+                    {
+                        "place_id": "26693344",
+                        "licence": "© LocationIQ.com CC BY 4.0, Data © OpenStreetMap contributors, ODbL 1.0",
+                        "osm_type": "node",
+                        "osm_id": "2525193585",
+                        "lat": "-37.870662",
+                        "lon": "144.9803321",
+                        "display_name": "Imbiss 25, Blessington Street, St Kilda, City of Port Phillip, Greater Melbourne, Victoria, 3182, Australia",
+                        "address": {
+                            "cafe": "Imbiss 25",
+                            "road": "Blessington Street",
+                            "suburb": "St Kilda",
+                            "county": "City of Port Phillip",
+                            "region": "Greater Melbourne",
+                            "state": "Victoria",
+                            "postcode": "3182",
+                            "country": "Australia",
+                            "country_code": "au"
+                        },
+                        "boundingbox": [
+                            "-37.870762",
+                            "-37.870562",
+                            "144.9802321",
+                            "144.9804321"
+                        ]
+                    }
+             */
+            var url =
+                $"https://us1.locationiq.com/v1/reverse.php?key={apiKey}&lat={origin.Latitude.ToString(CultureInfo.InvariantCulture)}&lon={origin.Longitude.ToString(CultureInfo.InvariantCulture)}&format=json";
+
+            var json = "";
+            try
+            {
+                var request = WebRequest.CreateHttp(url);
+                var response = await request.GetResponseAsync();
+                var stream = response.GetResponseStream();
+                var reader = new StreamReader(stream);
+                json = await reader.ReadToEndAsync();
+                var jsonObj = JObject.Parse(json);
+
+                var address = jsonObj["address"];
+                if (address == null)
+                {
+                    Err.ReportLogicError("Failed to lookup long/lat.", origin, "OriginLookup2");
+                    return;
+                }
+
+                /*  "address": {
+                        "cafe": "Imbiss 25",
+                        "road": "Blessington Street",
+                        "suburb": "St Kilda",
+                        "county": "City of Port Phillip",
+                        "region": "Greater Melbourne",
+                        "state": "Victoria",
+                        "postcode": "3182",
+                        "country": "Australia",
+                        "country_code": "au"
+                    }*/
+                origin.City = address["city"]?.Value();
+                origin.CountryCode = address["country_code"]?.Value();
+                origin.CountryName = address["country"]?.Value();
+                origin.RegionCode = address["region_code"]?.Value();
+                origin.RegionName = address["state"]?.Value();
+                origin.ZipCode = address["postcode"]?.Value();
+
+            }
+            catch (Exception exception)
+            {
+                throw new InvalidOperationException($"Failed to call lookupService or parse the JSON: {json}.", exception);
+            }
+
+
+        }
+
+        private async Task CommercialLookup(ErrorOrigin origin)
+        {
+            //fe80::1855:17f1:43ab:cc48%5 TODO: What do the %5 mean?
+            var pos = origin.IpAddress.IndexOf('%');
+            var ip = pos == -1 ? origin.IpAddress : origin.IpAddress.Substring(0, pos);
+
+            if (!IPAddress.TryParse(ip, out var address))
+                return;
+
+            if (address.IsInternal())
+            {
+                return;
+            }
+
+            var request = WebRequest.CreateHttp("https://timezoneapi.io/api/ip/?token=aAakJWmQjzgKudYTMmiV&ip=" + ip);
+            var json = "";
+            try
+            {
+                var response = await request.GetResponseAsync();
+                var stream = response.GetResponseStream();
+                var reader = new StreamReader(stream);
+                json = await reader.ReadToEndAsync();
+                var jsonObj = JObject.Parse(json);
+
+                /** Not found:
+                 * {"meta":{"code":"200","execution_time":"0.003025 seconds"},"data":{"ip":"151.248.19.34","city":"","postal":"","state":"","state_code":"","country":"","country_code":"","location":"","timezone":null,"datetime":null}}
+                 */
+
+                //data { "location": "51.5062,-0.0196 }
+                var location = (string)jsonObj["data"]["location"];
+                if (string.IsNullOrEmpty(location))
+                {
+                    Err.ReportLogicError("Failed to lookup long/lat.", origin, "OriginLookup");
+                    return;
+                }
+                    
+
+                var parts = location.Split(',');
+                var lat = double.Parse(parts[0], CultureInfo.InvariantCulture);
+                var lon = double.Parse(parts[1], CultureInfo.InvariantCulture);
+                var data = jsonObj["data"];
+                origin.City = data["city"].ToString();
+                origin.CountryCode = data["country_code"].ToString();
+                origin.Latitude = lat;
+                origin.Longitude = lon;
+                origin.CountryName = data["country"].ToString();
+                origin.RegionCode = data["state_code"].ToString();
+                origin.RegionName = data["state"].ToString();
+                origin.ZipCode = data["postal"].ToString();
+            }
+            catch (Exception exception)
+            {
+                throw new InvalidOperationException($"Failed to call lookupService or parse the JSON: {json}.", exception);
+            }
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/ErrorHashCode.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/ErrorHashCode.cs
new file mode 100644
index 00000000..d29a04a1
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/ErrorHashCode.cs
@@ -0,0 +1,21 @@
+namespace Coderr.Server.ReportAnalyzer.ErrorReports
+{
+    public class ErrorHashCode
+    {
+        /// 
+        /// Hashcode to use
+        /// 
+        public string HashCode { get; set; }
+
+        /// 
+        /// Used when two incidents have the same hash code to be able to separate them.
+        /// 
+        public string CollisionIdentifier { get; set; }
+
+
+        /// 
+        /// Used to be able to lookup older incidents when hashing algorithm changes (to avoid duplicates).
+        /// 
+        public string CompabilityHashSource { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashCodeGenerator.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashCodeGenerator.cs
new file mode 100644
index 00000000..92ce0b6f
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashCodeGenerator.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorReports
+{
+    /// 
+    ///     Used to generate hash codes for incoming error reports
+    /// 
+    /// 
+    ///     This new generator uses context information provided in exceptions to generate the hash code. For instance HTTP 404
+    ///     exception
+    ///     are based on the URI and not exception information
+    /// 
+    [ContainerService]
+    public class HashCodeGenerator : IHashCodeGenerator
+    {
+        private readonly IHashCodeSubGenerator[] _generators;
+        private static Regex _lineNumberRegEx = new Regex(RemoveLineNumbersRegEx, RegexOptions.Multiline);
+        private static readonly Regex FirstWordRegEx = new Regex(@"^[ \t]*([^\s]+) ", RegexOptions.Multiline);
+        private const string RemoveLineNumbersRegEx = @"^(.*)(:[\w]+ [\d]+)";
+        static List _cleanRegExes = new List();
+        private static List _replaceRegExes;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Specialized generators. treated as singletons.
+        public HashCodeGenerator(IEnumerable generators)
+        {
+            _generators = generators.ToArray();
+        }
+
+        /// 
+        ///     Generate a new hash code
+        /// 
+        /// entity
+        /// hash code
+        /// entity
+        public ErrorHashCode GenerateHashCode(ErrorReportEntity entity)
+        {
+            if (entity == null) throw new ArgumentNullException("entity");
+            foreach (var generator in _generators)
+            {
+                if (generator.CanGenerateFrom(entity))
+                {
+                    // forgiving ones so that we can get the report and process it with a default hash code instead.
+                    var code = generator.GenerateHashCode(entity);
+                    if (code != null)
+                        return code;
+                }
+            }
+
+            return DefaultCreateHashCode(entity);
+        }
+
+        /// 
+        ///     Method that will be invoked if no implementations of  generates an hash code.
+        /// 
+        /// received report
+        /// hash code
+        /// report
+        protected virtual ErrorHashCode DefaultCreateHashCode(ErrorReportEntity report)
+        {
+            if (report == null) throw new ArgumentNullException("report");
+
+
+            var hashSource = $"{report.Exception.FullName ?? report.Exception.Name}\r\n";
+            var foundHashSource = false;
+
+            // the client libraries can by themselves specify how we should identify
+            // unique incidents. We then use that identifier in combination with the exception name.
+            var collection = report.ContextCollections.FirstOrDefault(x => x.Name == "CoderrData");
+            if (collection != null)
+            {
+                if (collection.Properties.TryGetValue("HashSource", out var reportHashSource))
+                {
+                    var url = GetUrl(report.ContextCollections);
+
+                    //this is an workaround since our own Coderr Report vary url
+                    if (url?.StartsWith("/receiver/report/") != true)
+                    {
+                        foundHashSource = true;
+                        hashSource += reportHashSource;
+                    }
+                }
+            }
+            if (!foundHashSource)
+            {
+                // This identifier is determined by the developer when  the error is generated.
+                foreach (var contextCollection in report.ContextCollections)
+                {
+                    if (!contextCollection.Properties.TryGetValue("ErrorHashSource", out var ourHashSource))
+                        continue;
+
+                    hashSource = ourHashSource;
+                    foundHashSource = true;
+                    break;
+                }
+            }
+
+            var hashSourceForCompability = "";
+            if (!foundHashSource)
+            {
+                hashSourceForCompability = hashSource + CleanStackTrace(report.Exception.StackTrace ?? "", false);
+                hashSource += CleanStackTrace(report.Exception.StackTrace ?? "");
+
+            }
+
+            var hash = HashTheSource(hashSource);
+            return new ErrorHashCode
+            {
+                CollisionIdentifier = report.GenerateHashCodeIdentifier(),
+                HashCode = hash.ToString("X"),
+                CompabilityHashSource = hashSourceForCompability == "" ? null : HashTheSource(hashSourceForCompability).ToString("X")
+            };
+        }
+
+        private string GetUrl(IReadOnlyList contextCollections)
+        {
+            foreach (var collection in contextCollections)
+            {
+                if (collection.Properties.TryGetValue("Url", out var url))
+                {
+                    return url;
+                }
+            }
+
+            return null;
+        }
+
+        private static int HashTheSource(string hashSource)
+        {
+            var hash = 23;
+            foreach (var c in hashSource)
+            {
+                hash = hash * 31 + c;
+            }
+
+            return hash;
+        }
+
+        private static void EnsureRegExes()
+        {
+            if (_cleanRegExes.Count > 0)
+                return;
+
+            var linesToRemove = new[]
+            {
+                "---.*---\r?\n",
+                @"[ ]*at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw\(\)\r?\n",
+                @"[ ]*at System.Runtime.CompilerServices.TaskAwaiter.*\r?\n",
+                @"[ ]*at System.Threading.ExecutionContext.*\r?\n",
+                @"[ ]*at NServiceBus.*\r?\n",
+                @"[ ]*at RabbitMQ.*\r?\n",
+                @"[ ]*at Coderr.Client.*\r?\n",
+                @"[ ]*at .*d__+d+.MoveNext.*\r?\n",
+                @"[ ]*at System.Threading.Tasks.*\r?\n",
+                @"[ ]*at Microsoft.AspNetCore.Mvc.Internal.*\r?\n",
+                @"[ ]*at sun.reflect.*\r?\n",
+                @"[ ]*at java.lang.reflect.*\r?\n",
+                @"\$\$Lambda\$[\d]+\/[\d]+", //Java Lambda,
+                @" \[0x[a-f\d]+\] in \<[0-9a-z ]+\>:?\d*" // strange debug address for Java ;)
+            };
+            _cleanRegExes.Clear();
+            foreach (var rex in linesToRemove)
+            {
+                if (rex.EndsWith("$") || rex.StartsWith("^"))
+                    _cleanRegExes.Add(new Regex(rex, RegexOptions.Multiline));
+                else
+                    _cleanRegExes.Add(new Regex(rex));
+            }
+            
+            var linesToReplace = new[]
+            {
+                @"(\w+):\d+\)\r?$",//@"\.\w+(:\d+)\)$", // java line numbers
+                @"\<([a-zA-Z0-9_]+)\>d__[\d]+`?\d?.MoveNext",
+                @"(sun.reflect.GeneratedMethodAccessor)\d+"
+            };
+            _replaceRegExes = new List();
+            foreach (var rex in linesToReplace)
+            {
+                if (rex.EndsWith("$") || rex.StartsWith("^"))
+                    _replaceRegExes.Add(new Regex(rex, RegexOptions.Multiline));
+                else
+                    _replaceRegExes.Add(new Regex(rex));
+            }
+        }
+
+        internal static string CleanStackTrace(string stacktrace, bool withLanguageCleaner = true)
+        {
+            EnsureRegExes();
+            stacktrace = _lineNumberRegEx.Replace(stacktrace, "$1", 1000);
+            var eol = stacktrace.IndexOf("\r\n") == -1 ? "\n" : "\r\n";
+
+            foreach (var regEx in _cleanRegExes)
+            {
+                stacktrace = regEx.Replace(stacktrace, "", 100);
+            }
+            foreach (var regEx in _replaceRegExes)
+            {
+                stacktrace = regEx.Replace(stacktrace, "$1", 100);
+                stacktrace = regEx.Replace(stacktrace, OnEvaluate);
+            }
+
+            if (withLanguageCleaner)
+            {
+                stacktrace = FirstWordRegEx.Replace(stacktrace, "", 100);
+            }
+
+            return stacktrace;
+        }
+
+        private static string OnEvaluate(Match match)
+        {
+            return match.Captures[0].Value;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashcodeGenerators/HashCodeUtility.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/HashCodeUtility.cs
similarity index 96%
rename from src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashcodeGenerators/HashCodeUtility.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/HashCodeUtility.cs
index 6e0878aa..e3362e35 100644
--- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashcodeGenerators/HashCodeUtility.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/HashCodeUtility.cs
@@ -1,327 +1,327 @@
-namespace OneTrueError.ReportAnalyzer.Domain.Reports.HashcodeGenerators
-{
-    /// 
-    ///     Provides methods for manipulating and creating hash codes.
-    /// 
-    /// 
-    ///     
-    ///         This code is based on Bob Jenkins' public domain
-    ///         lookup3.c code.
-    ///     
-    ///     
-    ///         This work is hereby released into the Public Domain. To view a copy of the public domain dedication,
-    ///         visit http://creativecommons.org/licenses/publicdomain/ or send a letter to
-    ///         Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
-    ///     
-    /// 
-    public static class HashCodeUtility
-    {
-        /// 
-        ///     Combines the specified hash codes.
-        /// 
-        /// The first hash code.
-        /// The combined hash code.
-        /// This is a specialization of  for efficiency.
-        public static int CombineHashCodes(int hashCode1)
-        {
-            unchecked
-            {
-                var a = 0xdeadbeef + 4;
-                var b = a;
-                var c = a;
-
-                a += (uint) hashCode1;
-                FinalizeHash(ref a, ref b, ref c);
-
-                return (int) c;
-            }
-        }
-
-        /// 
-        ///     Combines the specified hash codes.
-        /// 
-        /// The first hash code.
-        /// The second hash code.
-        /// The combined hash code.
-        /// This is a specialization of  for efficiency.
-        public static int CombineHashCodes(int hashCode1, int hashCode2)
-        {
-            unchecked
-            {
-                var a = 0xdeadbeef + 8;
-                var b = a;
-                var c = a;
-
-                a += (uint) hashCode1;
-                b += (uint) hashCode2;
-                FinalizeHash(ref a, ref b, ref c);
-
-                return (int) c;
-            }
-        }
-
-        /// 
-        ///     Combines the specified hash codes.
-        /// 
-        /// The first hash code.
-        /// The second hash code.
-        /// The third hash code.
-        /// The combined hash code.
-        /// This is a specialization of  for efficiency.
-        public static int CombineHashCodes(int hashCode1, int hashCode2, int hashCode3)
-        {
-            unchecked
-            {
-                var a = 0xdeadbeef + 12;
-                var b = a;
-                var c = a;
-
-                a += (uint) hashCode1;
-                b += (uint) hashCode2;
-                c += (uint) hashCode3;
-                FinalizeHash(ref a, ref b, ref c);
-
-                return (int) c;
-            }
-        }
-
-        /// 
-        ///     Combines the specified hash codes.
-        /// 
-        /// The first hash code.
-        /// The second hash code.
-        /// The third hash code.
-        /// The fourth hash code.
-        /// The combined hash code.
-        /// This is a specialization of  for efficiency.
-        public static int CombineHashCodes(int hashCode1, int hashCode2, int hashCode3, int hashCode4)
-        {
-            unchecked
-            {
-                var a = 0xdeadbeef + 16;
-                var b = a;
-                var c = a;
-
-                a += (uint) hashCode1;
-                b += (uint) hashCode2;
-                c += (uint) hashCode3;
-                MixHash(ref a, ref b, ref c);
-
-                a += (uint) hashCode4;
-                FinalizeHash(ref a, ref b, ref c);
-
-                return (int) c;
-            }
-        }
-
-        /// 
-        ///     Combines the specified hash codes.
-        /// 
-        /// An array of hash codes.
-        /// The combined hash code.
-        /// 
-        ///     This method is based on the "hashword" function at http://burtleburtle.net/bob/c/lookup3.c. It attempts to
-        ///     thoroughly
-        ///     mix all the bits in the input hash codes.
-        /// 
-        public static int CombineHashCodes(params int[] hashCodes)
-        {
-            unchecked
-            {
-                // check for null
-                if (hashCodes == null)
-                    return 0x0d608219;
-
-                var length = hashCodes.Length;
-
-                var a = 0xdeadbeef + ((uint) length << 2);
-                var b = a;
-                var c = a;
-
-                var index = 0;
-                while (length - index > 3)
-                {
-                    a += (uint) hashCodes[index];
-                    b += (uint) hashCodes[index + 1];
-                    c += (uint) hashCodes[index + 2];
-                    MixHash(ref a, ref b, ref c);
-                    index += 3;
-                }
-
-                if (length - index > 2)
-                    c += (uint) hashCodes[index + 2];
-                if (length - index > 1)
-                    b += (uint) hashCodes[index + 1];
-
-                if (length - index > 0)
-                {
-                    a += (uint) hashCodes[index];
-                    FinalizeHash(ref a, ref b, ref c);
-                }
-
-                return (int) c;
-            }
-        }
-
-        /// 
-        ///     Gets a hash code for the specified ; this hash code is guaranteed not to change in the future.
-        /// 
-        /// The  to hash.
-        /// A hash code for the specified .
-        public static int GetPersistentHashCode(bool value)
-        {
-            // these values are the persistent hash codes for 0 and 1
-            return value ? -1266253386 : 1800329511;
-        }
-
-        /// 
-        ///     Gets a hash code for the specified ; this hash code is guaranteed not to change in the future.
-        /// 
-        /// The  to hash.
-        /// A hash code for the specified .
-        /// 
-        ///     Based on
-        ///     Robert Jenkins' 32 bit integer hash function.
-        /// 
-        public static int GetPersistentHashCode(int value)
-        {
-            unchecked
-            {
-                var hash = (uint) value;
-                hash = hash + 0x7ed55d16 + (hash << 12);
-                hash = hash ^ 0xc761c23c ^ (hash >> 19);
-                hash = hash + 0x165667b1 + (hash << 5);
-                hash = (hash + 0xd3a2646c) ^ (hash << 9);
-                hash = hash + 0xfd7046c5 + (hash << 3);
-                hash = hash ^ 0xb55a4f09 ^ (hash >> 16);
-                return (int) hash;
-            }
-        }
-
-        /// 
-        ///     Gets a hash code for the specified ; this hash code is guaranteed not to change in the future.
-        /// 
-        /// The  to hash.
-        /// A hash code for the specified .
-        /// Based on 64 bit to 32 bit Hash Functions.
-        public static int GetPersistentHashCode(long value)
-        {
-            unchecked
-            {
-                var hash = (ulong) value;
-                hash = ~hash + (hash << 18);
-                hash = hash ^ (hash >> 31);
-                hash = hash*21;
-                hash = hash ^ (hash >> 11);
-                hash = hash + (hash << 6);
-                hash = hash ^ (hash >> 22);
-                return (int) hash;
-            }
-        }
-
-        /// 
-        ///     Gets a hash code for the specified ; this hash code is guaranteed not to change in the future.
-        /// 
-        /// The  to hash.
-        /// A hash code for the specified .
-        /// Based on SuperFastHash.
-        public static int GetPersistentHashCode(string value)
-        {
-            unchecked
-            {
-                // check for degenerate input
-                if (string.IsNullOrEmpty(value))
-                    return 0;
-
-                var length = value.Length;
-                var hash = (uint) length;
-
-                var remainder = length & 1;
-                length >>= 1;
-
-                // main loop
-                var index = 0;
-                for (; length > 0; length--)
-                {
-                    hash += value[index];
-                    var temp = (uint) (value[index + 1] << 11) ^ hash;
-                    hash = (hash << 16) ^ temp;
-                    index += 2;
-                    hash += hash >> 11;
-                }
-
-                // handle odd string length
-                if (remainder == 1)
-                {
-                    hash += value[index];
-                    hash ^= hash << 11;
-                    hash += hash >> 17;
-                }
-
-                // force "avalanching" of final 127 bits
-                hash ^= hash << 3;
-                hash += hash >> 5;
-                hash ^= hash << 4;
-                hash += hash >> 17;
-                hash ^= hash << 25;
-                hash += hash >> 6;
-
-                return (int) hash;
-            }
-        }
-
-        // The "final()" macro from http://burtleburtle.net/bob/c/lookup3.c
-        private static void FinalizeHash(ref uint a, ref uint b, ref uint c)
-        {
-            unchecked
-            {
-                c ^= b;
-                c -= Rotate(b, 14);
-                a ^= c;
-                a -= Rotate(c, 11);
-                b ^= a;
-                b -= Rotate(a, 25);
-                c ^= b;
-                c -= Rotate(b, 16);
-                a ^= c;
-                a -= Rotate(c, 4);
-                b ^= a;
-                b -= Rotate(a, 14);
-                c ^= b;
-                c -= Rotate(b, 24);
-            }
-        }
-
-        // The "mix()" macro from http://burtleburtle.net/bob/c/lookup3.c
-        private static void MixHash(ref uint a, ref uint b, ref uint c)
-        {
-            unchecked
-            {
-                a -= c;
-                a ^= Rotate(c, 4);
-                c += b;
-                b -= a;
-                b ^= Rotate(a, 6);
-                a += c;
-                c -= b;
-                c ^= Rotate(b, 8);
-                b += a;
-                a -= c;
-                a ^= Rotate(c, 16);
-                c += b;
-                b -= a;
-                b ^= Rotate(a, 19);
-                a += c;
-                c -= b;
-                c ^= Rotate(b, 4);
-                b += a;
-            }
-        }
-
-        // The "rot()" macro from http://burtleburtle.net/bob/c/lookup3.c
-        private static uint Rotate(uint x, int k)
-        {
-            return (x << k) | (x >> (32 - k));
-        }
-    }
+namespace Coderr.Server.ReportAnalyzer.ErrorReports.HashcodeGenerators
+{
+    /// 
+    ///     Provides methods for manipulating and creating hash codes.
+    /// 
+    /// 
+    ///     
+    ///         This code is based on Bob Jenkins' public domain
+    ///         lookup3.c code.
+    ///     
+    ///     
+    ///         This work is hereby released into the Public Domain. To view a copy of the public domain dedication,
+    ///         visit http://creativecommons.org/licenses/publicdomain/ or send a letter to
+    ///         Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA.
+    ///     
+    /// 
+    public static class HashCodeUtility
+    {
+        /// 
+        ///     Combines the specified hash codes.
+        /// 
+        /// The first hash code.
+        /// The combined hash code.
+        /// This is a specialization of  for efficiency.
+        public static int CombineHashCodes(int hashCode1)
+        {
+            unchecked
+            {
+                var a = 0xdeadbeef + 4;
+                var b = a;
+                var c = a;
+
+                a += (uint) hashCode1;
+                FinalizeHash(ref a, ref b, ref c);
+
+                return (int) c;
+            }
+        }
+
+        /// 
+        ///     Combines the specified hash codes.
+        /// 
+        /// The first hash code.
+        /// The second hash code.
+        /// The combined hash code.
+        /// This is a specialization of  for efficiency.
+        public static int CombineHashCodes(int hashCode1, int hashCode2)
+        {
+            unchecked
+            {
+                var a = 0xdeadbeef + 8;
+                var b = a;
+                var c = a;
+
+                a += (uint) hashCode1;
+                b += (uint) hashCode2;
+                FinalizeHash(ref a, ref b, ref c);
+
+                return (int) c;
+            }
+        }
+
+        /// 
+        ///     Combines the specified hash codes.
+        /// 
+        /// The first hash code.
+        /// The second hash code.
+        /// The third hash code.
+        /// The combined hash code.
+        /// This is a specialization of  for efficiency.
+        public static int CombineHashCodes(int hashCode1, int hashCode2, int hashCode3)
+        {
+            unchecked
+            {
+                var a = 0xdeadbeef + 12;
+                var b = a;
+                var c = a;
+
+                a += (uint) hashCode1;
+                b += (uint) hashCode2;
+                c += (uint) hashCode3;
+                FinalizeHash(ref a, ref b, ref c);
+
+                return (int) c;
+            }
+        }
+
+        /// 
+        ///     Combines the specified hash codes.
+        /// 
+        /// The first hash code.
+        /// The second hash code.
+        /// The third hash code.
+        /// The fourth hash code.
+        /// The combined hash code.
+        /// This is a specialization of  for efficiency.
+        public static int CombineHashCodes(int hashCode1, int hashCode2, int hashCode3, int hashCode4)
+        {
+            unchecked
+            {
+                var a = 0xdeadbeef + 16;
+                var b = a;
+                var c = a;
+
+                a += (uint) hashCode1;
+                b += (uint) hashCode2;
+                c += (uint) hashCode3;
+                MixHash(ref a, ref b, ref c);
+
+                a += (uint) hashCode4;
+                FinalizeHash(ref a, ref b, ref c);
+
+                return (int) c;
+            }
+        }
+
+        /// 
+        ///     Combines the specified hash codes.
+        /// 
+        /// An array of hash codes.
+        /// The combined hash code.
+        /// 
+        ///     This method is based on the "hashword" function at http://burtleburtle.net/bob/c/lookup3.c. It attempts to
+        ///     thoroughly
+        ///     mix all the bits in the input hash codes.
+        /// 
+        public static int CombineHashCodes(params int[] hashCodes)
+        {
+            unchecked
+            {
+                // check for null
+                if (hashCodes == null)
+                    return 0x0d608219;
+
+                var length = hashCodes.Length;
+
+                var a = 0xdeadbeef + ((uint) length << 2);
+                var b = a;
+                var c = a;
+
+                var index = 0;
+                while (length - index > 3)
+                {
+                    a += (uint) hashCodes[index];
+                    b += (uint) hashCodes[index + 1];
+                    c += (uint) hashCodes[index + 2];
+                    MixHash(ref a, ref b, ref c);
+                    index += 3;
+                }
+
+                if (length - index > 2)
+                    c += (uint) hashCodes[index + 2];
+                if (length - index > 1)
+                    b += (uint) hashCodes[index + 1];
+
+                if (length - index > 0)
+                {
+                    a += (uint) hashCodes[index];
+                    FinalizeHash(ref a, ref b, ref c);
+                }
+
+                return (int) c;
+            }
+        }
+
+        /// 
+        ///     Gets a hash code for the specified ; this hash code is guaranteed not to change in the future.
+        /// 
+        /// The  to hash.
+        /// A hash code for the specified .
+        public static int GetPersistentHashCode(bool value)
+        {
+            // these values are the persistent hash codes for 0 and 1
+            return value ? -1266253386 : 1800329511;
+        }
+
+        /// 
+        ///     Gets a hash code for the specified ; this hash code is guaranteed not to change in the future.
+        /// 
+        /// The  to hash.
+        /// A hash code for the specified .
+        /// 
+        ///     Based on
+        ///     Robert Jenkins' 32 bit integer hash function.
+        /// 
+        public static int GetPersistentHashCode(int value)
+        {
+            unchecked
+            {
+                var hash = (uint) value;
+                hash = hash + 0x7ed55d16 + (hash << 12);
+                hash = hash ^ 0xc761c23c ^ (hash >> 19);
+                hash = hash + 0x165667b1 + (hash << 5);
+                hash = (hash + 0xd3a2646c) ^ (hash << 9);
+                hash = hash + 0xfd7046c5 + (hash << 3);
+                hash = hash ^ 0xb55a4f09 ^ (hash >> 16);
+                return (int) hash;
+            }
+        }
+
+        /// 
+        ///     Gets a hash code for the specified ; this hash code is guaranteed not to change in the future.
+        /// 
+        /// The  to hash.
+        /// A hash code for the specified .
+        /// Based on 64 bit to 32 bit Hash Functions.
+        public static int GetPersistentHashCode(long value)
+        {
+            unchecked
+            {
+                var hash = (ulong) value;
+                hash = ~hash + (hash << 18);
+                hash = hash ^ (hash >> 31);
+                hash = hash*21;
+                hash = hash ^ (hash >> 11);
+                hash = hash + (hash << 6);
+                hash = hash ^ (hash >> 22);
+                return (int) hash;
+            }
+        }
+
+        /// 
+        ///     Gets a hash code for the specified ; this hash code is guaranteed not to change in the future.
+        /// 
+        /// The  to hash.
+        /// A hash code for the specified .
+        /// Based on SuperFastHash.
+        public static int GetPersistentHashCode(string value)
+        {
+            unchecked
+            {
+                // check for degenerate input
+                if (string.IsNullOrEmpty(value))
+                    return 0;
+
+                var length = value.Length;
+                var hash = (uint) length;
+
+                var remainder = length & 1;
+                length >>= 1;
+
+                // main loop
+                var index = 0;
+                for (; length > 0; length--)
+                {
+                    hash += value[index];
+                    var temp = (uint) (value[index + 1] << 11) ^ hash;
+                    hash = (hash << 16) ^ temp;
+                    index += 2;
+                    hash += hash >> 11;
+                }
+
+                // handle odd string length
+                if (remainder == 1)
+                {
+                    hash += value[index];
+                    hash ^= hash << 11;
+                    hash += hash >> 17;
+                }
+
+                // force "avalanching" of final 127 bits
+                hash ^= hash << 3;
+                hash += hash >> 5;
+                hash ^= hash << 4;
+                hash += hash >> 17;
+                hash ^= hash << 25;
+                hash += hash >> 6;
+
+                return (int) hash;
+            }
+        }
+
+        // The "final()" macro from http://burtleburtle.net/bob/c/lookup3.c
+        private static void FinalizeHash(ref uint a, ref uint b, ref uint c)
+        {
+            unchecked
+            {
+                c ^= b;
+                c -= Rotate(b, 14);
+                a ^= c;
+                a -= Rotate(c, 11);
+                b ^= a;
+                b -= Rotate(a, 25);
+                c ^= b;
+                c -= Rotate(b, 16);
+                a ^= c;
+                a -= Rotate(c, 4);
+                b ^= a;
+                b -= Rotate(a, 14);
+                c ^= b;
+                c -= Rotate(b, 24);
+            }
+        }
+
+        // The "mix()" macro from http://burtleburtle.net/bob/c/lookup3.c
+        private static void MixHash(ref uint a, ref uint b, ref uint c)
+        {
+            unchecked
+            {
+                a -= c;
+                a ^= Rotate(c, 4);
+                c += b;
+                b -= a;
+                b ^= Rotate(a, 6);
+                a += c;
+                c -= b;
+                c ^= Rotate(b, 8);
+                b += a;
+                a -= c;
+                a ^= Rotate(c, 16);
+                c += b;
+                b -= a;
+                b ^= Rotate(a, 19);
+                a += c;
+                c -= b;
+                c ^= Rotate(b, 4);
+                b += a;
+            }
+        }
+
+        // The "rot()" macro from http://burtleburtle.net/bob/c/lookup3.c
+        private static uint Rotate(uint x, int k)
+        {
+            return (x << k) | (x >> (32 - k));
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/HttpErrorGenerator.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/HttpErrorGenerator.cs
new file mode 100644
index 00000000..67268083
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/HttpErrorGenerator.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Linq;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.ErrorReports;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorReports.HashcodeGenerators
+{
+    /// 
+    ///     Generates a hash code based on URLs and status code.
+    /// 
+    [ContainerService]
+    public class HttpErrorGenerator : IHashCodeSubGenerator
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(HttpErrorGenerator));
+
+
+        /// 
+        ///     Determines if this instance can generate a hashcode for the given entity.
+        /// 
+        /// entity to examine
+        /// true for HttpException; otherwise false.
+        /// entity
+        public bool CanGenerateFrom(ErrorReportEntity entity)
+        {
+            return entity.Exception.Name == "HttpException" ||
+                   entity.Exception.BaseClasses.Any(x => x.EndsWith("HttpException"));
+        }
+
+        /// 
+        ///     Generate a new hash code
+        /// 
+        /// entity
+        /// hashcode
+        /// entity
+        public ErrorHashCode GenerateHashCode(ErrorReportEntity entity)
+        {
+            string requestUrl = null;
+            var httpCode = 0;
+
+            foreach (var collection in entity.ContextCollections)
+            {
+                if (collection.Properties.TryGetValue("HttpCode", out var value))
+                {
+                    int.TryParse(value, out httpCode);
+
+                    // Server side errors are typically handled by other handlers.
+                    if (httpCode == 500)
+                        return null;
+                }
+
+                if (!collection.Properties.TryGetValue("RequestUrl", out requestUrl))
+                    collection.Properties.TryGetValue("Url", out requestUrl);
+
+            }
+
+            if (httpCode == 0 || string.IsNullOrWhiteSpace(requestUrl))
+                return null;
+
+            // Since this is for a specific application, remove host to make sure that errors
+            // for different environments/servers are treated as the same error.
+
+
+            // Remove host or correct uris
+            if (requestUrl.Contains("://"))
+            {
+                var pos2 = requestUrl.IndexOf("//");
+                pos2 = requestUrl.IndexOf("/", pos2 + 2);
+                requestUrl = requestUrl.Remove(0, pos2);
+            }
+
+            // and query string
+            var pos = requestUrl.IndexOf("?");
+            if (pos != -1)
+                requestUrl = requestUrl.Remove(pos);
+
+            return new ErrorHashCode
+            {
+                HashCode = HashCodeUtility.GetPersistentHashCode($"{httpCode};{requestUrl}").ToString("X"),
+                CollisionIdentifier = $"{httpCode};{requestUrl}"
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/LiveWriterGenerator.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/LiveWriterGenerator.cs
new file mode 100644
index 00000000..a7090c0b
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/LiveWriterGenerator.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Coderr.Server.Domain.Core.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorReports.HashcodeGenerators
+{
+    
+    class LiveWriterGenerator : IHashCodeSubGenerator
+    {
+        public bool CanGenerateFrom(ErrorReportEntity entity)
+        {
+            return false;
+        }
+
+        public ErrorHashCode GenerateHashCode(ErrorReportEntity entity)
+        {
+            return null;
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/TransactionLogFull.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/TransactionLogFull.cs
new file mode 100644
index 00000000..a72b1f37
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/HashcodeGenerators/TransactionLogFull.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorReports.HashcodeGenerators
+{
+    /// 
+    /// Groups SQL Server about the transaction log into a single error (instead of different errors depending on the stack trace).
+    /// 
+    [ContainerService]
+    class TransactionLogFull : IHashCodeSubGenerator
+    {
+        public bool CanGenerateFrom(ErrorReportEntity entity)
+        {
+            if (entity.Exception.Name != "SqlException")
+                return false;
+
+            if (!TryGetSqlErrorNumber(entity, out var numberStr)) return false;
+
+            return numberStr == "9002";
+        }
+
+        private static bool TryGetSqlErrorNumber(ErrorReportEntity entity, out string numberStr)
+        {
+            numberStr = null;
+            var collection = entity.ContextCollections.FirstOrDefault(x => x.Name == "ExceptionProperties");
+            return collection?.Properties.TryGetValue("Number", out numberStr) == true;
+        }
+
+        public ErrorHashCode GenerateHashCode(ErrorReportEntity entity)
+        {
+            if (!TryGetSqlErrorNumber(entity, out var numberStr))
+                return null;
+
+            return new ErrorHashCode
+            {
+                CollisionIdentifier = entity.Exception.Message,
+                HashCode = HashCodeUtility.GetPersistentHashCode(entity.Exception.Message).ToString("X")
+            };
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/IHashCodeGenerator.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/IHashCodeGenerator.cs
new file mode 100644
index 00000000..6563b409
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/IHashCodeGenerator.cs
@@ -0,0 +1,16 @@
+using System;
+using Coderr.Server.Domain.Core.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorReports
+{
+    public interface IHashCodeGenerator
+    {
+        /// 
+        ///     Generate a new hash code
+        /// 
+        /// entity
+        /// hash code
+        /// entity
+        ErrorHashCode GenerateHashCode(ErrorReportEntity entity);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/IHashCodeSubGenerator.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/IHashCodeSubGenerator.cs
similarity index 83%
rename from src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/IHashCodeSubGenerator.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/IHashCodeSubGenerator.cs
index 3af8cc3b..08771a72 100644
--- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/IHashCodeSubGenerator.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/IHashCodeSubGenerator.cs
@@ -1,31 +1,32 @@
-using System;
-
-namespace OneTrueError.ReportAnalyzer.Domain.Reports
-{
-    /// 
-    ///     Can be used to specialize the hash code generation which is used to tell if an exception is unique or not.
-    /// 
-    /// 
-    ///     
-    ///         For SQL Server exceptions it could use the SQL error code together with the stack trace for instance.
-    ///     
-    /// 
-    public interface IHashCodeSubGenerator
-    {
-        /// 
-        ///     Determines if this instance can generate a hashcode for the given entity.
-        /// 
-        /// entity to examine
-        /// true if a hashcode can be generated; otherwise false.
-        /// entity
-        bool CanGenerateFrom(ErrorReportEntity entity);
-
-        /// 
-        ///     Generate a new hash code
-        /// 
-        /// entity
-        /// hashcode
-        /// entity
-        string GenerateHashCode(ErrorReportEntity entity);
-    }
+using System;
+using Coderr.Server.Domain.Core.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorReports
+{
+    /// 
+    ///     Can be used to specialize the hash code generation which is used to tell if an exception is unique or not.
+    /// 
+    /// 
+    ///     
+    ///         For SQL Server exceptions it could use the SQL error code together with the stack trace for instance.
+    ///     
+    /// 
+    public interface IHashCodeSubGenerator
+    {
+        /// 
+        ///     Determines if this instance can generate a hashcode for the given entity.
+        /// 
+        /// entity to examine
+        /// true if a hashcode can be generated; otherwise false.
+        /// entity
+        bool CanGenerateFrom(ErrorReportEntity entity);
+
+        /// 
+        ///     Generate a new hash code
+        /// 
+        /// entity
+        /// hash code
+        /// entity
+        ErrorHashCode GenerateHashCode(ErrorReportEntity entity);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/ReportDecompressor.cs b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/ReportDecompressor.cs
new file mode 100644
index 00000000..d8be0983
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ErrorReports/ReportDecompressor.cs
@@ -0,0 +1,36 @@
+using System.IO;
+using System.IO.Compression;
+using System.Text;
+
+namespace Coderr.Server.ReportAnalyzer.ErrorReports
+{
+    /// 
+    ///     Decompresses report from GZIP compression
+    /// 
+    public class ReportDecompressor
+    {
+        /// 
+        ///     Deflate a compressed error report in JSON format
+        /// 
+        /// Compressed JSON errorReport
+        /// JSON string decompressed
+        public string Deflate(byte[] errorReport)
+        {
+            //owned and disposed by decompressor
+            var zipStream = new MemoryStream(errorReport);
+
+            using (var deflateStream = new MemoryStream())
+            {
+                using (var decompressor = new GZipStream(zipStream, CompressionMode.Decompress))
+                {
+                    decompressor.CopyTo(deflateStream);
+                    deflateStream.Position = 0;
+                    var buffer = new byte[deflateStream.Length];
+                    deflateStream.Read(buffer, 0, (int) deflateStream.Length);
+                    var strBuffer = Encoding.UTF8.GetString(buffer);
+                    return strBuffer;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Feedback/Handlers/AttachFeedbackToIncident.cs b/src/Server/Coderr.Server.ReportAnalyzer/Feedback/Handlers/AttachFeedbackToIncident.cs
new file mode 100644
index 00000000..a1bed354
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Feedback/Handlers/AttachFeedbackToIncident.cs
@@ -0,0 +1,40 @@
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.Feedback;
+using Coderr.Server.ReportAnalyzer.Abstractions.Feedback;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.Feedback.Handlers
+{
+    /// 
+    ///     Responsible of attaching feedback to incidents when the feedback was uploaded before the actual incident.
+    /// 
+    internal class AttachFeedbackToIncident : IMessageHandler
+    {
+        private readonly IFeedbackRepository _repository;
+
+        public AttachFeedbackToIncident(IFeedbackRepository repository)
+        {
+            _repository = repository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            var feedback = await _repository.FindPendingAsync(e.Report.ReportId);
+            if (feedback == null)
+                return;
+
+            feedback.AssignToReport(e.Report.Id, e.Incident.Id, e.Incident.ApplicationId);
+            var evt = new FeedbackAttachedToIncident
+            {
+                ApplicationId = e.Incident.ApplicationId,
+                ApplicationName = e.Incident.ApplicationName,
+                IncidentId = e.Incident.Id,
+                Message = feedback.Description,
+                UserEmailAddress = feedback.EmailAddress
+            };
+            await context.SendAsync(evt);
+            await _repository.UpdateAsync(feedback);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Feedback/Handlers/StoreFeedbackFromNewReports.cs b/src/Server/Coderr.Server.ReportAnalyzer/Feedback/Handlers/StoreFeedbackFromNewReports.cs
new file mode 100644
index 00000000..00fee56e
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Feedback/Handlers/StoreFeedbackFromNewReports.cs
@@ -0,0 +1,50 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Feedback.Commands;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.Feedback.Handlers
+{
+    /// 
+    ///     Responsible of separating the feedback from the incident when it's uploaded as context data.
+    /// 
+    public class StoreFeedbackFromNewReports : IMessageHandler
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(StoreFeedbackFromNewReports));
+
+        /// 
+        ///     Process an event asynchronously.
+        /// 
+        /// event to process
+        /// 
+        ///     Task to wait on.
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            try
+            {
+                var userInfo = e.Report.ContextCollections.FirstOrDefault(x => x.Name == "UserSuppliedInformation");
+                if (userInfo == null)
+                    return;
+
+                userInfo.Properties.TryGetValue("Description", out var description);
+                userInfo.Properties.TryGetValue("Email", out var email);
+                _logger.Debug($"queueing feedback attached to report {e.Report.ReportId}: {email} {description}");
+                var cmd = new SubmitFeedback(e.Report.ReportId, e.Report.RemoteAddress ?? "")
+                {
+                    Feedback = description,
+                    Email = email
+                };
+
+                await context.SendAsync(cmd);
+            }
+            catch (Exception exception)
+            {
+                _logger.Error("Failed for " + e.Report.ReportId, exception);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Feedback/IUserFeedbackRepository.cs b/src/Server/Coderr.Server.ReportAnalyzer/Feedback/IUserFeedbackRepository.cs
new file mode 100644
index 00000000..1a4cf173
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Feedback/IUserFeedbackRepository.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Coderr.Server.ReportAnalyzer.Feedback
+{
+    public interface IUserFeedbackRepository
+    {
+        Task CreateAsync(NewFeedback feedback);
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Feedback/NewFeedback.cs b/src/Server/Coderr.Server.ReportAnalyzer/Feedback/NewFeedback.cs
new file mode 100644
index 00000000..aee82765
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Feedback/NewFeedback.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Coderr.Server.Api.Core.Feedback.Commands;
+
+namespace Coderr.Server.ReportAnalyzer.Feedback
+{
+    public class NewFeedback
+    {
+         /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        /// Client side id.
+        /// The remote address.
+        /// 
+        ///     errorId
+        ///     or
+        ///     remoteAddress
+        /// 
+        public NewFeedback(string errorId, string remoteAddress)
+        {
+            if (errorId == null) throw new ArgumentNullException("errorId");
+            if (remoteAddress == null) throw new ArgumentNullException("remoteAddress");
+            RemoteAddress = remoteAddress;
+            ErrorId = errorId;
+            CreatedAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        /// Error report identity.
+        /// The remote address.
+        /// 
+        ///     remoteAddress
+        /// 
+        /// reportId
+        public NewFeedback(int reportId, string remoteAddress)
+        {
+            if (remoteAddress == null) throw new ArgumentNullException("remoteAddress");
+            if (reportId <= 0) throw new ArgumentOutOfRangeException("reportId");
+
+            RemoteAddress = remoteAddress;
+            ReportId = reportId;
+            CreatedAtUtc = DateTime.UtcNow;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected NewFeedback()
+        {
+        }
+
+        /// 
+        ///     When the feedback was created in the client library
+        /// 
+        public DateTime CreatedAtUtc { get; set; }
+
+        /// 
+        ///     Email address (user want to get status updates)
+        /// 
+        public string Email { get; set; }
+
+        /// 
+        ///     Error id generated in our client library. Used to identify error reports before they have been saved into our
+        ///     system
+        /// 
+        public string ErrorId { get; private set; }
+
+        /// 
+        ///     Error description
+        /// 
+        public string Feedback { get; set; }
+
+        /// 
+        ///     IP that the user connected from. either taken from the error report or from the HTTP POST if the UI less client
+        ///     library directed the user to our web site.
+        /// 
+        public string RemoteAddress { get; set; }
+
+        /// 
+        ///     PK from the db entry of the error report.
+        /// 
+        public int ReportId { get; private set; }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/IAnalyticsRepository.cs b/src/Server/Coderr.Server.ReportAnalyzer/IAnalyticsRepository.cs
new file mode 100644
index 00000000..a5d9a062
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/IAnalyticsRepository.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Incidents;
+
+namespace Coderr.Server.ReportAnalyzer
+{
+    /// 
+    ///     Repository (think CQRS write side in this case. yay!)
+    /// 
+    public interface IAnalyticsRepository
+    {
+        /// 
+        /// Save an environment
+        /// 
+        /// incident that the report is for
+        /// Name as specified by the developer
+        void SaveEnvironmentName(int incidentId, int applicationId, string environmentName);
+
+        /// 
+        ///     Create a new incident
+        /// 
+        /// incident to persist
+        /// incentAnalysis
+        void CreateIncident(IncidentBeingAnalyzed incidentAnalysis);
+
+        /// 
+        ///     Create a new error report
+        /// 
+        /// report to persist
+        /// report
+        void CreateReport(ErrorReportEntity report);
+
+        /// 
+        ///     There is an incident for the given report error id.
+        /// 
+        /// error id for a report, generated in the client library
+        /// true if an incident exists; otherwise false.
+        /// clientReportId
+        bool ExistsByClientId(string clientReportId);
+
+        /// 
+        ///     Find incident
+        /// 
+        /// application that the incident belongs to
+        /// generated hash code
+        /// 
+        ///     Line to use if multiple incidents (typically first line in the exception error
+        ///     message) have the same hash code
+        /// 
+        /// incident if found; otherwise null.
+        IncidentBeingAnalyzed FindIncidentForReport(int applicationId, string reportHashCode, string hashCodeIdentifier);
+
+        /// 
+        ///     Get application name
+        /// 
+        /// application id
+        /// name
+        string GetAppName(int applicationId);
+
+        /// 
+        ///     Update incident
+        /// 
+        /// incident to persist
+        /// incidentAnalysis
+        void UpdateIncident(IncidentBeingAnalyzed incidentAnalysis);
+
+        /// 
+        /// Number of reports received this month
+        /// 
+        /// 
+        int GetMonthReportCount();
+
+        void AddMissedReport(DateTime date);
+        Task StoreReportStats(ReportMapping mapping);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/DuplicateChecker.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/DuplicateChecker.cs
new file mode 100644
index 00000000..0c49d649
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/DuplicateChecker.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    public class DuplicateChecker
+    {
+        private Dictionary _lastReportIndex = new Dictionary();
+
+        public DuplicateChecker()
+        {
+        }
+
+        public bool IsDuplicate(string remoteAddress, NewReportDTO report)
+        {
+            lock (_lastReportIndex)
+            {
+                // duplicate
+                if (_lastReportIndex.TryGetValue(report.ReportId, out var when))
+                {
+                    _lastReportIndex[report.ReportId] = DateTime.UtcNow;
+                    return true;
+                }
+
+                if (_lastReportIndex.Count >= 100)
+                {
+                    var idsToRemove = _lastReportIndex.OrderBy(x => x.Value).Take(10);
+                    foreach (var id in idsToRemove)
+                    {
+                        _lastReportIndex.Remove(id.Key);
+                    }
+
+                    _lastReportIndex[report.ReportId] = DateTime.UtcNow;
+                }
+            }
+
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/FilterResult.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/FilterResult.cs
new file mode 100644
index 00000000..c463e3f6
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/FilterResult.cs
@@ -0,0 +1,35 @@
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    /// 
+    ///     Result for .
+    /// 
+    public enum FilterResult
+    {
+        /// 
+        ///     Process it completely (storing and analyzing).
+        /// 
+        FullAnalyzis,
+
+        /// 
+        ///     Send the  event but do not store the report.
+        /// 
+        /// 
+        ///     
+        ///         Typically when we reached a limit for the current incident.
+        ///     
+        /// 
+        ProcessAndDiscard,
+
+        /// 
+        ///     Report should not be processed at all.
+        /// 
+        /// 
+        ///     
+        ///         For instance when the report is received in an environment that should be ignored (like "Development").
+        ///     
+        /// 
+        DiscardReport
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/FilterService.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/FilterService.cs
new file mode 100644
index 00000000..308de925
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/FilterService.cs
@@ -0,0 +1,56 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Client.Contracts;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Incidents;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    /// 
+    ///     Uses a bunch of filters to determine of the inbound error report should be analyzed (and/or being processed by
+    ///     events internally)
+    /// 
+    [ContainerService]
+    public class FilterService : IFilterService
+    {
+        private readonly IReadOnlyList _filters;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(FilterService));
+
+
+        public FilterService(IEnumerable filters)
+        {
+            _filters = filters.ToList();
+        }
+
+        public async Task CanProcess(ErrorReportEntity report, IncidentBeingAnalyzed incident)
+        {
+            var recommendedAction = FilterResult.FullAnalyzis;
+            var context = new FilterContext {ErrorReport = report};
+            foreach (var filter in _filters)
+            {
+                var result = await filter.Filter(context);
+                if (result == FilterResult.FullAnalyzis)
+                {
+                    continue;
+                }
+
+                _logger.Debug("Filter " + filter + " want to " + result);
+
+                // We want to pick the worst recommendation.
+                if (result == FilterResult.DiscardReport)
+                {
+                    recommendedAction = FilterResult.DiscardReport;
+                }
+                else if (recommendedAction == FilterResult.FullAnalyzis)
+                {
+                    recommendedAction = FilterResult.ProcessAndDiscard;
+                }
+            }
+
+            return recommendedAction;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessFeedbackHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessFeedbackHandler.cs
new file mode 100644
index 00000000..6b527a2b
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessFeedbackHandler.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Feedback.Commands;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands;
+using log4net;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound.Handlers
+{
+    public class ProcessFeedbackHandler : IMessageHandler
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(ProcessFeedbackHandler));
+
+        public async Task HandleAsync(IMessageContext context, ProcessFeedback message)
+        {
+            try
+            {
+                var submitCmd = new SubmitFeedback(message.ReportId, message.RemoteAddress)
+                {
+                    CreatedAtUtc = message.ReceivedAtUtc,
+                    Email = message.EmailAddress,
+                    Feedback = message.Description
+                };
+                await context.SendAsync(submitCmd);
+            }
+            catch (Exception ex)
+            {
+                _logger.Error("Failed to process " + JsonConvert.SerializeObject(message), ex);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessReportHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessReportHandler.cs
new file mode 100644
index 00000000..0978c2b9
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ProcessReportHandler.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Commands;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound.Handlers
+{
+    public class ProcessReportHandler : IMessageHandler
+    {
+        private readonly Reports.IReportAnalyzer _analyzer;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(ProcessReportHandler));
+
+        public ProcessReportHandler(Reports.IReportAnalyzer analyzer)
+        {
+            _analyzer = analyzer;
+        }
+
+
+        public async Task HandleAsync(IMessageContext context, ProcessReport message)
+        {
+            try
+            {
+                ErrorReportException ex = null;
+                if (message.Exception != null)
+                    ex = ConvertException(message.Exception);
+                var contexts = message.ContextCollections.Select(ConvertContext).ToList();
+                ConvertContextDataToCollections(contexts);
+
+                var entity = new ErrorReportEntity(message.ApplicationId, message.ReportId, message.CreatedAtUtc, ex,
+                    contexts)
+                {
+                    RemoteAddress = message.RemoteAddress,
+                    EnvironmentName = message.EnvironmentName
+                };
+
+
+                await _analyzer.Analyze(context, entity);
+
+                // 0 = we ignored the report.
+                if (entity.Id > 0)
+                {
+                    await ProcessLogEntries(context, message, entity);
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.Error("Failed to analyze report ", ex);
+            }
+        }
+
+        private static void ConvertContextDataToCollections(ICollection contexts)
+        {
+            var collection = contexts.FirstOrDefault(x => x.Name == "ContextData");
+            if (collection == null)
+            {
+                return;
+            }
+
+            foreach (var property in collection.Properties)
+            {
+                var pos = property.Key.IndexOf('.');
+                if (pos == -1 || pos == property.Key.Length - 1)
+                {
+                    continue;
+                }
+
+                var contextName = property.Key.Substring(0, pos);
+                var propertyName = property.Key.Substring(pos + 1);
+                var newContext = contexts.FirstOrDefault(x => x.Name == contextName);
+                if (newContext == null)
+                {
+                    newContext = new ErrorReportContextCollection(contextName,
+                        new Dictionary());
+                    contexts.Add(newContext);
+                }
+
+                newContext.Properties[propertyName] = property.Value;
+            }
+        }
+
+        private async Task ProcessLogEntries(IMessageContext context, ProcessReport message, ErrorReportEntity entity)
+        {
+            if (message.LogEntries == null || message.LogEntries.Length == 0)
+            {
+                return;
+            }
+
+            var logEntries = message.LogEntries
+                .Select(x => new StoreLogEntriesEntry
+                {
+                    Message = x.Message,
+                    Level = (StoreLogEntriesLogLevel)x.LogLevel,
+                    Exception = x.Exception,
+                    TimeStampUtc = x.TimestampUtc
+                })
+                .ToArray();
+            var cmd = new StoreLogEntries(entity.IncidentId, entity.Id, logEntries);
+            await context.SendAsync(cmd);
+        }
+
+
+        private ErrorReportContextCollection ConvertContext(ProcessReportContextInfoDto arg)
+        {
+            return new ErrorReportContextCollection(arg.Name, arg.Properties);
+        }
+
+        private ErrorReportException ConvertException(ProcessReportExceptionDto dto)
+        {
+            var entity = new ErrorReportException
+            {
+                Message = dto.Message,
+                FullName = dto.FullName,
+                Name = dto.Name,
+                AssemblyName = dto.AssemblyName,
+                BaseClasses = dto.BaseClasses,
+                Everything = dto.Everything,
+                Namespace = dto.Namespace,
+                StackTrace = dto.StackTrace
+            };
+            if (dto.InnerExceptionDto != null)
+                entity.InnerException = ConvertException(dto.InnerExceptionDto);
+            return entity;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ReOpenIncidentHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ReOpenIncidentHandler.cs
new file mode 100644
index 00000000..11864f02
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/ReOpenIncidentHandler.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Incidents.Commands;
+using Coderr.Server.Domain.Core.Incidents;
+using Coderr.Server.Domain.Core.Incidents.Events;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound.Handlers
+{
+    /// 
+    ///     Uses the incident repository and the domain entity to apply the change.
+    /// 
+    public class ReOpenIncidentHandler : IMessageHandler
+    {
+        private readonly IDomainQueue _domainQueue;
+        private readonly IIncidentRepository _repository;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// To be able to load and update incident
+        public ReOpenIncidentHandler(IIncidentRepository repository, IDomainQueue domainQueue)
+        {
+            _repository = repository ?? throw new ArgumentNullException(nameof(repository));
+            _domainQueue = domainQueue;
+        }
+
+
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReOpenIncident command)
+        {
+            var incident = await _repository.GetAsync(command.IncidentId);
+            incident.Reopen();
+            await _repository.UpdateAsync(incident);
+
+            var evt = new IncidentReOpened(incident.ApplicationId, incident.Id, DateTime.UtcNow);
+            await context.SendAsync(evt);
+            await _domainQueue.PublishAsync(context.Principal, evt);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/IReportAnalyzer.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/IReportAnalyzer.cs
new file mode 100644
index 00000000..fe331a9a
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/IReportAnalyzer.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.ErrorReports;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound.Handlers.Reports
+{
+    public interface IReportAnalyzer
+    {
+        /// 
+        ///     Analyze report
+        /// 
+        /// report
+        /// report
+        Task Analyze(IMessageContext context, ErrorReportEntity report);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportAnalyzer.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportAnalyzer.cs
new file mode 100644
index 00000000..1ffd83ac
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportAnalyzer.cs
@@ -0,0 +1,352 @@
+using System;
+using System.Diagnostics;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Abstractions.Reports;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.Domain.Core.Incidents.Events;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using Coderr.Server.ReportAnalyzer.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Incidents;
+using DotNetCqs;
+using log4net;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound.Handlers.Reports
+{
+    /// 
+    ///     Runs analysis for the report.
+    /// 
+    [ContainerService]
+    public class ReportAnalyzer : IReportAnalyzer
+    {
+        public const string AppAssemblyVersion = "AppAssemblyVersion";
+        private readonly IHashCodeGenerator _hashCodeGenerator;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(ReportAnalyzer));
+        private readonly IAnalyticsRepository _repository;
+        private readonly IDomainQueue _domainQueue;
+        private readonly IConfiguration _reportConfig;
+        private readonly IFilterService _filterService;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Used to identify is this is a new unique exception
+        /// 
+        ///     to publish the
+        ///      event
+        /// 
+        /// repos
+        public ReportAnalyzer(IHashCodeGenerator hashCodeGenerator, IAnalyticsRepository repository, IDomainQueue domainQueue, IConfiguration reportConfig, IFilterService filterService)
+        {
+            _hashCodeGenerator = hashCodeGenerator;
+            _repository = repository;
+            _domainQueue = domainQueue;
+            _reportConfig = reportConfig;
+            _filterService = filterService;
+        }
+
+        /// 
+        ///     Analyze report
+        /// 
+        /// report
+        /// report
+        public async Task Analyze(IMessageContext context, ErrorReportEntity report)
+        {
+            if (report == null) throw new ArgumentNullException(nameof(report));
+
+            /* Not in LIVE
+            var countThisMonth = _repository.GetMonthReportCount();
+            if (countThisMonth >= 500)
+            {
+                _repository.AddMissedReport(DateTime.Today);
+                return;
+            }
+            */
+
+            _logger.Debug("Running as " + context.Principal.ToFriendlyString());
+            var exists = _repository.ExistsByClientId(report.ClientReportId);
+            if (exists)
+            {
+                _logger.Warn($"Report have already been uploaded: {report.ClientReportId} for {report.RemoteAddress}.");
+                return;
+            }
+
+            ErrorHashCode hashcodeResult;
+            try
+            {
+                hashcodeResult = _hashCodeGenerator.GenerateHashCode(report);
+                report.Init(hashcodeResult.HashCode);
+            }
+            catch (Exception ex)
+            {
+                var reportJson = JsonConvert.SerializeObject(report);
+                if (reportJson.Length > 1000000)
+                    reportJson = reportJson.Substring(0, 100000) + "[....]";
+                _logger.Fatal($"Failed to init report {reportJson}", ex);
+                return;
+            }
+
+            var storeReport = true;
+            var applicationVersion = GetVersionFromReport(report);
+            var isReOpened = false;
+
+            IncidentBeingAnalyzed incident = null;
+            if (hashcodeResult.CompabilityHashSource != null)
+            {
+                incident = _repository.FindIncidentForReport(report.ApplicationId, hashcodeResult.CompabilityHashSource, hashcodeResult.CollisionIdentifier);
+                if (incident != null)
+                {
+                    report.Init(hashcodeResult.CompabilityHashSource);
+                }
+            }
+            if (incident == null)
+            {
+                incident = _repository.FindIncidentForReport(report.ApplicationId, report.ReportHashCode, hashcodeResult.CollisionIdentifier);
+            }
+
+            var result = await _filterService.CanProcess(report, incident);
+            if (result == FilterResult.DiscardReport)
+            {
+                return;
+            }
+
+            if (result == FilterResult.ProcessAndDiscard)
+            {
+                storeReport = false;
+            }
+
+            var isNewIncident = false;
+            if (incident == null)
+            {
+                if (report.Exception == null)
+                    _logger.Debug("Got no exception");
+
+                isNewIncident = true;
+                incident = BuildIncident(report);
+                _repository.CreateIncident(incident);
+                await _repository.StoreReportStats(new ReportMapping()
+                {
+                    IncidentId = incident.Id,
+                    ErrorId = report.ClientReportId,
+                    ReceivedAtUtc = report.CreatedAtUtc
+                });
+
+                var evt = new IncidentCreated(incident.ApplicationId,
+                    incident.Id, incident.Description, incident.FullName)
+                {
+                    CreatedAtUtc = incident.CreatedAtUtc,
+                    ApplicationVersion = applicationVersion,
+                };
+                _logger.Info($"Storing IncidentCreated with {context.Principal.ToFriendlyString()}: {JsonConvert.SerializeObject(evt)}");
+                await _domainQueue.PublishAsync(context.Principal, evt);
+                await context.SendAsync(evt);
+            }
+            else
+            {
+                if (incident.IsIgnored)
+                {
+                    _logger.Info("Incident is ignored: " + JsonConvert.SerializeObject(report));
+                    incident.WasJustIgnored();
+                    _repository.UpdateIncident(incident);
+                    report.IncidentId = incident.Id;
+                    return;
+                }
+
+                // Do this before checking closed
+                // as we want to see if it still gets reports.
+                var stat = new ReportMapping()
+                {
+                    IncidentId = incident.Id,
+                    ErrorId = report.ClientReportId,
+                    ReceivedAtUtc = report.CreatedAtUtc
+                };
+                await _repository.StoreReportStats(stat);
+                _logger.Debug("Storing stats " + JsonConvert.SerializeObject(stat));
+
+                if (incident.IsClosed)
+                {
+                    if (applicationVersion != null && incident.IsReportIgnored(applicationVersion))
+                    {
+                        _logger.Info("Ignored report since it's for a version less that the solution version: " + JsonConvert.SerializeObject(report));
+                        incident.WasJustIgnored();
+                        _repository.UpdateIncident(incident);
+                        report.IncidentId = incident.Id;
+                        return;
+                    }
+
+                    isReOpened = true;
+                    incident.ReOpen();
+                    var evt = new IncidentReOpened(incident.ApplicationId, incident.Id,
+                        incident.CreatedAtUtc)
+                    {
+                        ApplicationVersion = applicationVersion
+                    };
+                    await context.SendAsync(evt);
+                    await _domainQueue.PublishAsync(context.Principal, evt);
+                }
+
+                incident.AddReport(report);
+
+                // Let's continue to receive reports once a day when
+                // limit is reached (to get more fresh data, and still not load the system unnecessary).
+                var timesSinceLastReport = DateTime.UtcNow.Subtract(incident.LastStoredReportUtc);
+                if (incident.ReportCount > _reportConfig.Value.MaxReportsPerIncident
+                    && timesSinceLastReport < TimeSpan.FromMinutes(10))
+                {
+                    _repository.UpdateIncident(incident);
+                    _logger.Debug($"Report count is more than {_reportConfig.Value.MaxReportsPerIncident}. Ignoring reports for incident {incident.Id}. Minutes since last report: " + timesSinceLastReport.TotalMinutes);
+                    storeReport = false;
+                    //don't exit here, since we want to be able to process reports
+                }
+            }
+
+            if (!string.IsNullOrWhiteSpace(report.EnvironmentName))
+                _repository.SaveEnvironmentName(incident.Id, incident.ApplicationId, report.EnvironmentName);
+
+            report.IncidentId = incident.Id;
+
+            if (storeReport)
+            {
+                incident.LastStoredReportUtc = DateTime.UtcNow;
+                _repository.UpdateIncident(incident);
+                _repository.CreateReport(report);
+                _logger.Debug($"saving report {report.Id} for incident {incident.Id}");
+            }
+
+            var appName = _repository.GetAppName(incident.ApplicationId);
+            var summary = new IncidentSummaryDTO(incident.Id, incident.Description)
+            {
+                ApplicationId = incident.ApplicationId,
+                ApplicationName = appName,
+                CreatedAtUtc = incident.CreatedAtUtc,
+                LastUpdateAtUtc = incident.UpdatedAtUtc,
+                IsReOpened = incident.IsReOpened,
+                Name = incident.Description,
+                ReportCount = incident.ReportCount
+            };
+            var sw = new Stopwatch();
+            sw.Start();
+            var e = new ReportAddedToIncident(summary, ConvertToCoreReport(report, applicationVersion), isReOpened)
+            {
+                IsNewIncident = isNewIncident,
+                IsStored = storeReport,
+                EnvironmentName = report.EnvironmentName
+            };
+            await context.SendAsync(e);
+
+            if (storeReport)
+            {
+                await context.SendAsync(new ProcessInboundContextCollections());
+            }
+
+            if (sw.ElapsedMilliseconds > 200)
+                _logger.Debug($"PublishAsync took {sw.ElapsedMilliseconds}");
+            sw.Stop();
+        }
+
+        private string GetVersionFromReport(ErrorReportEntity report)
+        {
+            foreach (var contextCollection in report.ContextCollections)
+            {
+                if (contextCollection.Properties.TryGetValue(AppAssemblyVersion, out var version))
+                    return version;
+            }
+
+            return null;
+        }
+
+        private IncidentBeingAnalyzed BuildIncident(ErrorReportEntity entity)
+        {
+            if (entity.Exception == null)
+                return new IncidentBeingAnalyzed(entity);
+
+            if (entity.Exception.Name == "AggregateException")
+            {
+                try
+                {
+                    var exception = entity.Exception;
+
+                    //TODO: Check if there are more than one InnerExceptions and then abort this specialization.
+                    while (exception != null && exception.Name == "AggregateException")
+                    {
+                        exception = exception.InnerException;
+                    }
+
+                    var incident = new IncidentBeingAnalyzed(entity, exception);
+                    return incident;
+                }
+                catch
+                {
+                }
+            }
+
+            if (entity.Exception.Name == "ReflectionTypeLoadException")
+            {
+                try
+                {
+                    var item = JObject.Parse(entity.Exception.Everything);
+                    var i = new IncidentBeingAnalyzed(entity);
+                    var items = (JObject)item["LoaderExceptions"];
+                    var exception = items.First;
+
+
+                    //var incident = new Incident(entity, exception);
+                    //incident.AddIncidentTags(new[] { "ReflectionTypeLoadException" });
+                    //return incident;
+
+                    //TODO: load LoaderExceptions which is an Exception[] array
+                }
+                catch
+                {
+                }
+            }
+
+            return new IncidentBeingAnalyzed(entity);
+        }
+
+        private ReportExeptionDTO ConvertToCoreException(ErrorReportException exception)
+        {
+            var ex = new ReportExeptionDTO
+            {
+                AssemblyName = exception.AssemblyName,
+                BaseClasses = exception.BaseClasses,
+                Everything = exception.Everything,
+                FullName = exception.FullName,
+                Message = exception.Message,
+                Name = exception.Name,
+                Namespace = exception.Namespace,
+                StackTrace = exception.StackTrace
+            };
+            if (ex.InnerException != null)
+                ex.InnerException = ConvertToCoreException(exception.InnerException);
+            return ex;
+        }
+
+        private ReportDTO ConvertToCoreReport(ErrorReportEntity report, string version)
+        {
+            var dto = new ReportDTO
+            {
+                ApplicationId = report.ApplicationId,
+                ContextCollections =
+                    report.ContextCollections.Select(x => new ContextCollectionDTO(x.Name, x.Properties)).ToArray(),
+                Id = report.Id,
+                CreatedAtUtc = report.CreatedAtUtc,
+                IncidentId = report.IncidentId,
+                RemoteAddress = report.RemoteAddress,
+                ReportId = report.ClientReportId,
+                ReportVersion = "1",
+                ApplicationVersion = version
+            };
+            if (report.Exception != null)
+                dto.Exception = ConvertToCoreException(report.Exception);
+            return dto;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportDtoConverter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportDtoConverter.cs
new file mode 100644
index 00000000..99508c7d
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/Handlers/Reports/ReportDtoConverter.cs
@@ -0,0 +1,74 @@
+using System.Linq;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound.Handlers.Reports
+{
+    /// 
+    ///     Converts DTOs from the client library format to our internal DTO.
+    /// 
+    public class ReportDtoConverter
+    {
+        /// 
+        ///     Convert exception to our internal format
+        /// 
+        /// exception
+        /// our format
+        public ErrorReportException ConvertException(ProcessReportExceptionDto exceptionDto)
+        {
+            var ex = new ErrorReportException
+            {
+                AssemblyName = exceptionDto.AssemblyName,
+                BaseClasses = exceptionDto.BaseClasses,
+                Everything = exceptionDto.Everything,
+                FullName = exceptionDto.FullName,
+                Message = exceptionDto.Message,
+                Name = exceptionDto.Name,
+                Namespace = exceptionDto.Namespace,
+                StackTrace = exceptionDto.StackTrace
+            };
+            if (exceptionDto.InnerExceptionDto != null)
+                ex.InnerException = ConvertException(exceptionDto.InnerExceptionDto);
+            return ex;
+        }
+
+        /// 
+        ///     Convert received report to our internal format
+        /// 
+        /// client report
+        /// application that we identified that the report belongs to
+        /// internal format
+        public ErrorReportEntity ConvertReport(ProcessReport report, int applicationId)
+        {
+            ErrorReportException ex = null;
+            if (report.Exception != null)
+            {
+                ex = ConvertException(report.Exception);
+            }
+
+            //var id = _idGeneratorClient.GetNextId(ErrorReportEntity.SEQUENCE);
+            var contexts = report.ContextCollections.Select(x => new ErrorReportContextCollection(x.Name, x.Properties)).ToArray();
+            var dto = new ErrorReportEntity(applicationId, report.ReportId, report.CreatedAtUtc, ex, contexts);
+            return dto;
+        }
+
+        /// 
+        ///     Deserialize a client library formatted report
+        /// 
+        /// JSON
+        /// DTO
+        public ProcessReport LoadReportFromJson(string json)
+        {
+            var report =
+                JsonConvert.DeserializeObject(json, new JsonSerializerSettings
+                {
+                    ObjectCreationHandling = ObjectCreationHandling.Auto,
+                    TypeNameHandling = TypeNameHandling.Auto,
+                    ContractResolver = new Server.ReportAnalyzer.IncludeNonPublicMembersContractResolver(),
+                    ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
+                });
+            return report;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/IFilterService.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/IFilterService.cs
new file mode 100644
index 00000000..4fc0a3c1
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/IFilterService.cs
@@ -0,0 +1,12 @@
+using System.Threading.Tasks;
+using Coderr.Client.Contracts;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Incidents;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    public interface IFilterService
+    {
+        Task CanProcess(ErrorReportEntity report, IncidentBeingAnalyzed incident);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/IncludeNonPublicMembersContractResolver.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/IncludeNonPublicMembersContractResolver.cs
new file mode 100644
index 00000000..4f068cce
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/IncludeNonPublicMembersContractResolver.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    public class IncludeNonPublicMembersContractResolver : DefaultContractResolver
+    {
+        //protected override List GetSerializableMembers(Type objectType)
+        //{
+        //    var members = base.GetSerializableMembers(objectType);
+        //    return members.Where(m => !m.Name.EndsWith("k__BackingField")).ToList();
+        //}
+
+        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
+        {
+            //TODO: Maybe cache
+            var prop = base.CreateProperty(member, memberSerialization);
+
+            if (!prop.Writable)
+            {
+                var property = member as PropertyInfo;
+                if (property != null)
+                {
+                    var hasPrivateSetter = property.GetSetMethod(true) != null;
+                    prop.Writable = hasPrivateSetter;
+                }
+            }
+
+            return prop;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReadMe.md b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReadMe.md
new file mode 100644
index 00000000..8cfeb186
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReadMe.md
@@ -0,0 +1,5 @@
+Inbound DTO handling
+====================
+
+These classes are used to recieve, deserialize and validate reports from the client libraries.
+Once done they are saved into a queue for further processing and analysis.
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportDecompressor.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportDecompressor.cs
new file mode 100644
index 00000000..9c917981
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportDecompressor.cs
@@ -0,0 +1,31 @@
+using System.IO;
+using System.IO.Compression;
+using System.Text;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    public class ReportDecompressor
+    {
+        /// 
+        ///     Deflate a compressed error report in JSON format
+        /// 
+        /// Compressed JSON errorReport
+        /// JSON string decompressed
+        public string Deflate(byte[] errorReport)
+        {
+            var zipStream = new MemoryStream(errorReport);
+            using (var deflateStream = new MemoryStream())
+            {
+                using (var decompressor = new GZipStream(zipStream, CompressionMode.Decompress))
+                {
+                    decompressor.CopyTo(deflateStream);
+                    deflateStream.Position = 0;
+                    var buffer = new byte[deflateStream.Length];
+                    deflateStream.Read(buffer, 0, (int) deflateStream.Length);
+                    var strBuffer = Encoding.UTF8.GetString(buffer);
+                    return strBuffer;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportFilter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportFilter.cs
new file mode 100644
index 00000000..8560975b
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportFilter.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Client.Contracts;
+using Coderr.Server.Domain.Core.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    public interface IReportFilter
+    {
+        Task Filter(FilterContext context);
+    }
+
+    public class FilterContext
+    {
+        public ErrorReportEntity ErrorReport { get; set; }
+    }
+}
diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Models/ReportValidator.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportValidator.cs
similarity index 95%
rename from src/Server/OneTrueError.Web/Areas/Receiver/Models/ReportValidator.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportValidator.cs
index 8a916c5a..f4019590 100644
--- a/src/Server/OneTrueError.Web/Areas/Receiver/Models/ReportValidator.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/ReportValidator.cs
@@ -1,40 +1,40 @@
-using System;
-using System.Security.Cryptography;
-using System.Text;
-
-namespace OneTrueError.Web.Areas.Receiver.Models
-{
-    /// 
-    ///     Used to make sure that the uploaded report was signed with the correct shared secret.
-    /// 
-    public class ReportValidator
-    {
-        /// 
-        ///     Validate HTTP body
-        /// 
-        /// Shared secret associated with the AppKey.
-        /// Signature that the client have generated.
-        /// HTTP body
-        /// true if the specifiedSignature was generated with the shared secret; otherwise false.
-        public static bool ValidateBody(string sharedSecret, string specifiedSignature, byte[] body)
-        {
-            if (sharedSecret == null) throw new ArgumentNullException("sharedSecret");
-            if (specifiedSignature == null) throw new ArgumentNullException("specifiedSignature");
-            if (body == null) throw new ArgumentNullException("body");
-
-            var hashAlgo = new HMACSHA256(Encoding.UTF8.GetBytes(sharedSecret.ToLower()));
-            var hash = hashAlgo.ComputeHash(body);
-            var signature = Convert.ToBase64String(hash);
-
-            var hashAlgo1 = new HMACSHA256(Encoding.UTF8.GetBytes(sharedSecret.ToUpper()));
-            var hash1 = hashAlgo1.ComputeHash(body);
-            var signature1 = Convert.ToBase64String(hash1);
-
-
-            // uri encoding :(
-            specifiedSignature = specifiedSignature.Replace(' ', '+');
-
-            return specifiedSignature.Equals(signature) || specifiedSignature.Equals(signature1);
-        }
-    }
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    /// 
+    ///     Used to make sure that the uploaded report was signed with the correct shared secret.
+    /// 
+    public class ReportValidator
+    {
+        /// 
+        ///     Validate HTTP body
+        /// 
+        /// Shared secret associated with the AppKey.
+        /// Signature that the client have generated.
+        /// HTTP body
+        /// true if the specifiedSignature was generated with the shared secret; otherwise false.
+        public static bool ValidateBody(string sharedSecret, string specifiedSignature, byte[] body)
+        {
+            if (sharedSecret == null) throw new ArgumentNullException("sharedSecret");
+            if (specifiedSignature == null) throw new ArgumentNullException("specifiedSignature");
+            if (body == null) throw new ArgumentNullException("body");
+
+            var hashAlgo = new HMACSHA256(Encoding.UTF8.GetBytes(sharedSecret.ToLower()));
+            var hash = hashAlgo.ComputeHash(body);
+            var signature = Convert.ToBase64String(hash);
+
+            var hashAlgo1 = new HMACSHA256(Encoding.UTF8.GetBytes(sharedSecret.ToUpper()));
+            var hash1 = hashAlgo1.ComputeHash(body);
+            var signature1 = Convert.ToBase64String(hash1);
+
+
+            // uri encoding :(
+            specifiedSignature = specifiedSignature.Replace(' ', '+');
+
+            return specifiedSignature.Equals(signature) || specifiedSignature.Equals(signature1);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Inbound/SaveReportHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/SaveReportHandler.cs
new file mode 100644
index 00000000..5331f75f
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Inbound/SaveReportHandler.cs
@@ -0,0 +1,334 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Security.Authentication;
+using System.Security.Claims;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands;
+using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Models;
+using DotNetCqs;
+using DotNetCqs.Queues;
+using Griffin.Data;
+using log4net;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.ReportAnalyzer.Inbound
+{
+    /// 
+    ///     Validates inbound report and store it in our internal queue for analysis.
+    /// 
+    public class SaveReportHandler
+    {
+        private static readonly DuplicateChecker DuplicateChecker = new DuplicateChecker();
+        private static readonly Dictionary _lastReportPerApplication = new Dictionary();
+        private readonly List> _filters = new List>();
+        private readonly ILog _logger = LogManager.GetLogger(typeof(SaveReportHandler));
+        private readonly int _maxSizeForJsonErrorReport;
+        private readonly IMessageQueue _queue;
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Queue to store inbound reports in
+        /// queueProvider;connectionFactory
+        public SaveReportHandler(IMessageQueue queue, IAdoNetUnitOfWork unitOfWork, IReportConfig reportConfig)
+        {
+            _unitOfWork = unitOfWork;
+            _queue = queue ?? throw new ArgumentNullException(nameof(queue));
+            _maxSizeForJsonErrorReport = reportConfig.MaxReportJsonSize;
+        }
+
+        public void AddFilter(Func filter)
+        {
+            if (filter == null) throw new ArgumentNullException(nameof(filter));
+            _filters.Add(filter);
+        }
+
+        public async Task BuildReportAsync(ClaimsPrincipal user, string appKey, string signatureProvidedByTheClient,
+            string remoteAddress,
+            byte[] reportBody)
+        {
+            if (!Guid.TryParse(appKey, out var tempKey))
+            {
+                _logger.Warn("Incorrect appKeyFormat: " + appKey + " from " + remoteAddress);
+                throw new InvalidCredentialException("AppKey must be a valid GUID which '" + appKey + "' is not.");
+            }
+
+            var application = await GetAppAsync(appKey);
+            if (application == null)
+            {
+                _logger.Warn($"Unknown appKey: {appKey} from {remoteAddress}");
+                throw new InvalidCredentialException($"AppKey was not found in the database. Key '{appKey}'.");
+            }
+
+            lock (_lastReportPerApplication)
+            {
+                if (_lastReportPerApplication.TryGetValue(application.Id, out var limiter))
+                {
+                    if (limiter.IsRateExceeded())
+                    {
+                        _logger.Info($"Rate limit is exceeded for application {application.Id}.");
+                        return;
+                    }
+                        
+                    limiter.AddReport();
+                }
+                else
+                {
+                    _lastReportPerApplication[application.Id] = new RateLimit();
+                }
+            }
+
+
+            // web(js) applications do not sign the body
+            if (signatureProvidedByTheClient != null && !ReportValidator.ValidateBody(application.SharedSecret,
+                signatureProvidedByTheClient, reportBody))
+            {
+                await StoreInvalidReportAsync(appKey, signatureProvidedByTheClient, remoteAddress, reportBody);
+                throw new AuthenticationException(
+                    "You either specified the wrong SharedSecret, or someone tampered with the data.");
+            }
+
+            var report = DeserializeBody(reportBody);
+            if (report == null)
+                return;
+
+            if (Debugger.IsAttached) _logger.Debug("Received " + JsonConvert.SerializeObject(report));
+
+            // Ignore our weird error where the HashSource is incorrect
+            if (report.Exception?.Message.Contains("Slow POST /receiver/report") == true) return;
+
+            if (DuplicateChecker.IsDuplicate(remoteAddress, report))
+            {
+                _logger.Debug($"Duplicate report {report.ReportId} from {remoteAddress}");
+                return;
+            }
+
+            // correct incorrect clients
+            if (report.CreatedAtUtc > DateTime.UtcNow)
+                report.CreatedAtUtc = DateTime.UtcNow;
+
+            if (_filters.Any(x => !x(report)))
+                return;
+
+            var internalDto = new ProcessReport
+            {
+                ApplicationId = application.Id,
+                RemoteAddress = remoteAddress,
+                ContextCollections = report.ContextCollections.Select(ConvertCollection).ToArray(),
+                CreatedAtUtc = report.CreatedAtUtc,
+                DateReceivedUtc = DateTime.UtcNow,
+                EnvironmentName = report.EnvironmentName,
+                Exception = ConvertException(report.Exception),
+                LogEntries = ConvertLogEntries(report.LogEntries),
+                ReportId = report.ReportId,
+                ReportVersion = report.ReportVersion
+            };
+
+            await StoreReportAsync(user, internalDto);
+        }
+
+        private static ProcessReportContextInfoDto ConvertCollection(NewReportContextInfo arg)
+        {
+            return new ProcessReportContextInfoDto(arg.Name, arg.Properties);
+        }
+
+        private static ProcessReportExceptionDto ConvertException(NewReportException exception)
+        {
+            var ex = new ProcessReportExceptionDto
+            {
+                Name = exception.Name,
+                AssemblyName = exception.AssemblyName,
+                BaseClasses = exception.BaseClasses,
+                Everything = exception.Everything,
+                FullName = exception.FullName,
+                Message = exception.Message,
+                Namespace = exception.Namespace,
+                Properties = exception.Properties,
+                StackTrace = exception.StackTrace
+            };
+            if (exception.InnerException != null)
+                ex.InnerExceptionDto = ConvertException(exception.InnerException);
+            return ex;
+        }
+
+        private ProcessReportLogEntry[] ConvertLogEntries(NewReportLogEntry[] dtos)
+        {
+            if (dtos == null) return null;
+
+            return dtos
+                .Select(x => new ProcessReportLogEntry
+                {
+                    Message = x.Message,
+                    Exception = x.Exception,
+                    LogLevel = x.LogLevel,
+                    Source = x.Source,
+                    TimestampUtc = x.TimestampUtc
+                })
+                .ToArray();
+        }
+
+        private NewReportDTO DeserializeBody(byte[] body)
+        {
+            string json;
+            if (body[0] == 0x1f && body[1] == 0x8b)
+            {
+                var decompressor = new ReportDecompressor();
+                json = decompressor.Deflate(body);
+            }
+            else
+            {
+                json = Encoding.UTF8.GetString(body);
+            }
+
+            // protection against very large error reports.
+            if (json.Length > _maxSizeForJsonErrorReport)
+                return null;
+
+            // to support clients that still use the OneTrueError client library.
+            json = json.Replace("OneTrueError", "Coderr");
+
+            var dto = JsonConvert.DeserializeObject(json,
+                new JsonSerializerSettings
+                {
+                    TypeNameHandling = TypeNameHandling.Objects,
+                    ContractResolver =
+                        new IncludeNonPublicMembersContractResolver()
+                });
+
+            //if (Debugger.IsAttached && Environment.MachineName.IndexOf("jg", StringComparison.OrdinalIgnoreCase) != -1)
+            //    File.WriteAllText($@"C:\Temp\Report_{dto.Exception.Name}.{DateTime.Now:yyyyMMdd_hhmmss_fff}.json",
+            //        json);
+
+            if (string.IsNullOrEmpty(dto.EnvironmentName) && !string.IsNullOrEmpty(dto.Environment))
+                dto.EnvironmentName = dto.Environment;
+
+            // Safeguard against malformed reports (other clients than the built in ones)
+            if (dto.Exception == null)
+                return null;
+            if (string.IsNullOrWhiteSpace(dto.Exception.Name) && string.IsNullOrWhiteSpace(dto.Exception.FullName))
+                return null;
+            if (string.IsNullOrWhiteSpace(dto.Exception.Name))
+                dto.Exception.Name = dto.Exception.FullName;
+            if (string.IsNullOrWhiteSpace(dto.Exception.FullName))
+                dto.Exception.FullName = dto.Exception.Name;
+            if (dto.Exception.BaseClasses == null)
+                dto.Exception.BaseClasses = new string[0];
+            if (dto.Exception.Namespace == null)
+                dto.Exception.Namespace = "";
+
+            return dto;
+        }
+
+        private async Task GetAppAsync(string appKey)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT Id, SharedSecret FROM Applications WHERE AppKey = @key OR AppKey = @key2";
+                cmd.AddParameter("key", appKey);
+                cmd.AddParameter("key2", appKey.Replace("-", ""));
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    if (!await reader.ReadAsync())
+                        return null;
+
+                    return new AppInfo {Id = reader.GetInt32(0), SharedSecret = reader.GetString(1)};
+                }
+            }
+        }
+
+        private async Task StoreInvalidReportAsync(string appKey, string sig, string remoteAddress, byte[] reportBody)
+        {
+            try
+            {
+                //TODO: Make something generic.
+                using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+                {
+                    cmd.CommandText =
+                        @"INSERT INTO InvalidReports(appkey, signature, reportbody, errormessage, createdatutc)
+                                            VALUES (@appkey, @signature, @reportbody, @errormessage, @createdatutc);";
+                    cmd.AddParameter("appKey", appKey);
+                    cmd.AddParameter("signature", sig);
+                    var p = cmd.CreateParameter();
+                    p.DbType = DbType.Binary;
+                    p.ParameterName = "reportbody";
+                    p.Value = reportBody;
+                    cmd.Parameters.Add(p);
+                    //cmd.AddParameter("reportbody", reportBody);
+                    cmd.AddParameter("errormessage", "Failed to validate signature");
+                    cmd.AddParameter("createdatutc", DateTime.UtcNow);
+                    await cmd.ExecuteNonQueryAsync();
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.Error("Failed to save invalid report.", ex);
+            }
+        }
+
+        private async Task StoreReportAsync(ClaimsPrincipal user, ProcessReport report)
+        {
+            try
+            {
+                using (var session = _queue.BeginSession())
+                {
+                    await session.EnqueueAsync(user, new Message(report));
+                    await session.SaveChanges();
+                }
+            }
+            catch (Exception ex)
+            {
+                _logger.Error(
+                    "Failed to StoreReport: " + JsonConvert.SerializeObject(new {model = report}), ex);
+            }
+        }
+
+        private class RateLimit
+        {
+            private readonly LinkedList _reportDates = new LinkedList();
+
+            public RateLimit()
+            {
+                _reportDates.AddLast(DateTime.UtcNow);
+            }
+
+            public void AddReport()
+            {
+                lock (_reportDates)
+                {
+                    _reportDates.AddLast(DateTime.UtcNow);
+                }
+            }
+
+            public bool IsRateExceeded()
+            {
+                if (Debugger.IsAttached)
+                {
+                    return false;
+                }
+
+                var twentySecondsAgo = DateTime.UtcNow.AddSeconds(-20);
+                lock (_reportDates)
+                {
+                    while (_reportDates.First != null && _reportDates.First.Value < twentySecondsAgo)
+                        _reportDates.RemoveFirst();
+
+                    return _reportDates.Count > 20;
+                }
+            }
+        }
+
+        private class AppInfo
+        {
+            public int Id { get; set; }
+            public string SharedSecret { get; set; }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentBeingAnalyzed.cs b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentBeingAnalyzed.cs
new file mode 100644
index 00000000..29862d87
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentBeingAnalyzed.cs
@@ -0,0 +1,252 @@
+using System;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.Infrastructure;
+using Coderr.Server.ReportAnalyzer.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.Incidents
+{
+    /// 
+    ///     Keeps track of all report occurrences for a single incident (i.e. error reports which generates the same hash code)
+    /// 
+    public class IncidentBeingAnalyzed
+    {
+        private string _description;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        protected IncidentBeingAnalyzed()
+        {
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// 
+        /// entity
+        /// entity have no hashcode
+        public IncidentBeingAnalyzed(ErrorReportEntity entity)
+        {
+            if (entity == null) throw new ArgumentNullException("entity");
+            if (string.IsNullOrEmpty(entity.ReportHashCode))
+                throw new ArgumentException("ReportHashCode must be specified to be able to identify duplicates.");
+            Description = entity.Exception.Message;
+            FullName = entity.Exception.FullName;
+            StackTrace = entity.Exception.StackTrace;
+
+            AddReport(entity);
+            ReportHashCode = entity.ReportHashCode;
+            HashCodeIdentifier = entity.GenerateHashCodeIdentifier();
+            ApplicationId = entity.ApplicationId;
+            UpdatedAtUtc = entity.CreatedAtUtc;
+            CreatedAtUtc = entity.CreatedAtUtc;
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// entity
+        /// exception to analyze
+        /// entity; exception
+        /// entity.hashcode is null
+        public IncidentBeingAnalyzed(ErrorReportEntity entity, ErrorReportException exception)
+        {
+            if (entity == null) throw new ArgumentNullException("entity");
+            if (exception == null) throw new ArgumentNullException("exception");
+            if (string.IsNullOrEmpty(entity.ReportHashCode))
+                throw new ArgumentException("ReportHashCode must be specified to be able to identify duplicates.");
+
+            Description = exception.Message;
+            FullName = exception.FullName;
+            StackTrace = HashCodeGenerator.CleanStackTrace(exception.StackTrace);
+
+            AddReport(entity);
+            ReportHashCode = entity.ReportHashCode;
+            HashCodeIdentifier = entity.GenerateHashCodeIdentifier();
+            ApplicationId = entity.ApplicationId;
+            UpdatedAtUtc = entity.CreatedAtUtc;
+            CreatedAtUtc = entity.CreatedAtUtc;
+        }
+
+        /// 
+        ///     Application that the report belongs in
+        /// 
+        public int ApplicationId { get; private set; }
+
+        /// 
+        ///     When report was created
+        /// 
+        public DateTime CreatedAtUtc { get; private set; }
+
+        /// 
+        ///     Incident description
+        /// 
+        public string Description
+        {
+            get
+            {
+                if (string.IsNullOrEmpty(_description))
+                    return "Ooops Error!";
+
+                return _description;
+            }
+            set => _description = value;
+        }
+
+        /// 
+        ///     List of all environment names that the developer specified when reporting the errors.
+        /// 
+        public string[] EnvironmentNames { get; set; }
+
+        /// 
+        ///     Full name of the exception message.
+        /// 
+        public string FullName { get; private set; }
+
+        /// 
+        ///     Used to identify this incident when the hash code is the same as for other incidents.
+        /// 
+        /// 
+        public string HashCodeIdentifier { get; private set; }
+
+        /// 
+        ///     primary key
+        /// 
+        public int Id { get; private set; }
+
+        /// 
+        ///     Version that this incident should not be ignored in
+        /// 
+        /// 
+        ///     
+        ///         Both used in close incident and ignore incident
+        ///     
+        /// 
+        public string IgnoredUntilVersion { get; set; }
+
+        /// 
+        ///     Incident have been solved (bug as been identified and corrected)
+        /// 
+        public bool IsClosed => State == AnalyzedIncidentState.Closed;
+
+        /// 
+        ///     Incident is ignored, i.e. do not track any more reports or send any notifications.
+        /// 
+        public bool IsIgnored => State == AnalyzedIncidentState.Ignored;
+
+        /// 
+        ///     Incident is opened again after being closed.
+        /// 
+        public bool IsReOpened { get; set; }
+
+        /// 
+        ///     When we received a report (just to keep track of how fresh this incident is).
+        /// 
+        public DateTime LastReportAtUtc { get; set; }
+
+        /// 
+        ///     When we received a report that we actual stored.
+        /// 
+        /// 
+        ///     
+        ///         This field is used to determine if we should store another report today, while 
+        ///         is just used to keep track of when we received the most recent report.
+        ///     
+        /// 
+        public DateTime LastStoredReportUtc { get; set; }
+
+        /// 
+        ///     Set if incident was closed and a solution was written
+        /// 
+        public DateTime PreviousSolutionAtUtc { get; set; }
+
+        /// 
+        ///     When the report was opened again
+        /// 
+        /// 
+        /// -
+        public DateTime ReOpenedAtUtc { get; set; }
+
+        /// 
+        ///     Total number of reports for this incident (including those who was ignored)
+        /// 
+        public int ReportCount { get; set; }
+
+        /// 
+        ///     Hashcode identifying this incident
+        /// 
+        public string ReportHashCode { get; private set; }
+
+        /// 
+        ///     When the solution was written
+        /// 
+        public DateTime SolvedAtUtc { get; set; }
+
+        /// 
+        ///     Stack trace from the exception
+        /// 
+        public string StackTrace { get; set; }
+
+        public AnalyzedIncidentState State { get; private set; }
+
+
+        /// 
+        ///     Incident has been updated (received a new report or by an action by a user)
+        /// 
+        public DateTime UpdatedAtUtc { get; private set; }
+
+        /// 
+        ///     Add another report.
+        /// 
+        /// entity
+        /// entity
+        public void AddReport(ErrorReportEntity entity)
+        {
+            if (entity == null) throw new ArgumentNullException("entity");
+
+            if (string.IsNullOrWhiteSpace(StackTrace) && entity.Exception != null)
+            {
+                Description = entity.Exception.Message;
+                FullName = entity.Exception.FullName;
+                StackTrace = entity.Exception.StackTrace;
+            }
+
+            LastReportAtUtc = entity.CreatedAtUtc;
+            ReportCount++;
+        }
+
+        /// 
+        ///     Check if this incident is ignored in the version that we received in the newest error report.
+        /// 
+        /// Version in the report that we just received.
+        /// 
+        public bool IsReportIgnored(string reportedVersion)
+        {
+            if (reportedVersion == null) throw new ArgumentNullException(nameof(reportedVersion));
+            var comparer = new ApplicationVersionComparer();
+            return comparer.Compare(reportedVersion, IgnoredUntilVersion) < 0;
+        }
+
+        /// 
+        ///     Open a closed incident.
+        /// 
+        /// 
+        public void ReOpen()
+        {
+            PreviousSolutionAtUtc = SolvedAtUtc;
+            State = AnalyzedIncidentState.New;
+            ReOpenedAtUtc = DateTime.UtcNow;
+            UpdatedAtUtc = DateTime.UtcNow;
+            IsReOpened = true;
+        }
+
+        /// 
+        ///     Just ignored a new report
+        /// 
+        public void WasJustIgnored()
+        {
+            LastReportAtUtc = DateTime.UtcNow;
+            ReportCount++;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentCreatedHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentCreatedHandler.cs
new file mode 100644
index 00000000..5d26ec8b
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentCreatedHandler.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.Incidents;
+using Coderr.Server.Domain.Core.Incidents.Events;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.Incidents
+{
+    class IncidentCreatedHandler : IMessageHandler
+    {
+        private IIncidentRepository _incidentRepository;
+
+        public IncidentCreatedHandler(IIncidentRepository incidentRepository)
+        {
+            _incidentRepository = incidentRepository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident message)
+        {
+            if (message.IsNewIncident != true)
+                return;
+            var collection = message.Report.GetCoderrCollection();
+            if (collection == null)
+                return;
+
+            if (!collection.Properties.TryGetValue("CorrelationId", out var correlationId))
+                return;
+
+            await _incidentRepository.MapCorrelationId(message.Incident.Id, correlationId);
+        }
+    }
+}
diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Incidents/IncidentHashMapEntry.cs b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentHashMapEntry.cs
similarity index 93%
rename from src/Server/OneTrueError.ReportAnalyzer/Domain/Incidents/IncidentHashMapEntry.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentHashMapEntry.cs
index 8ca98676..a56ea30e 100644
--- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Incidents/IncidentHashMapEntry.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentHashMapEntry.cs
@@ -1,63 +1,63 @@
-//using System;
-//using System.Collections.Generic;
-//using System.Linq;
-//using OneTrueError.ReportAnalyzer.Domain.Reports;
-
-//namespace OneTrueError.ReportAnalyzer.Domain.Incidents
-//{
-//    /// 
-//    ///     Different error reports can get the same hash code.
-//    ///     this entity is used to be able to find the correct incident by mapping exception full names to incidentIds.
-//    ///     (we might have to store the stack trace instead).
-//    /// 
-//    public class IncidentHashMapEntry
-//    {
-//        /// 
-//        /// Creates a new instance of .
-//        /// 
-//        public IncidentHashMapEntry()
-//        {
-//            IncidentFullPaths = new Dictionary();
-//        }
-
-
-//        /// 
-//        ///     Map where the key is incident.FirstStacktraceLine
-//        /// 
-//        public Dictionary IncidentFullPaths { get; private set; }
-
-//        /// 
-//        /// Generate a hashcode from an incident
-//        /// 
-//        /// 
-//        /// 
-//        /// 
-//        public string GetIncidentId(ErrorReportEntity entity)
-//        {
-//            if (IncidentFullPaths.Count == 1)
-//                return IncidentFullPaths.Values.First();
-
-//            if (entity.Exception == null)
-//                throw new InvalidOperationException(string.Format("Failed to exception in entity: {0}", entity.Id));
-
-//            if (entity.Exception.StackTrace == null)
-//                throw new InvalidOperationException(string.Format("Failed to stack trace in entity: {0}", entity.Id));
-
-//            int pos = entity.Exception.StackTrace.IndexOf("\r\n");
-//            if (pos == -1)
-//                throw new InvalidOperationException(
-//                    string.Format("Failed to find first line in stack trace: {0} for entity {1}",
-//                        entity.Exception.StackTrace, entity.Id));
-
-//            string firstLine = entity.Exception.StackTrace.Substring(0, pos);
-
-//            return IncidentFullPaths[firstLine];
-//        }
-
-//        public void Add(string incidentId, string firstLineInStacktrace)
-//        {
-//            IncidentFullPaths[firstLineInStacktrace] = incidentId;
-//        }
-//    }
-//}
-
+//using System;
+//using System.Collections.Generic;
+//using System.Linq;
+//using Coderr.ReportAnalyzer.Domain.Reports;
+
+//namespace Coderr.ReportAnalyzer.Domain.Incidents
+//{
+//    /// 
+//    ///     Different error reports can get the same hash code.
+//    ///     this entity is used to be able to find the correct incident by mapping exception full names to incidentIds.
+//    ///     (we might have to store the stack trace instead).
+//    /// 
+//    public class IncidentHashMapEntry
+//    {
+//        /// 
+//        /// Creates a new instance of .
+//        /// 
+//        public IncidentHashMapEntry()
+//        {
+//            IncidentFullPaths = new Dictionary();
+//        }
+
+
+//        /// 
+//        ///     Map where the key is incident.FirstStacktraceLine
+//        /// 
+//        public Dictionary IncidentFullPaths { get; private set; }
+
+//        /// 
+//        /// Generate a hashcode from an incident
+//        /// 
+//        /// 
+//        /// 
+//        /// 
+//        public string GetIncidentId(ErrorReportEntity entity)
+//        {
+//            if (IncidentFullPaths.Count == 1)
+//                return IncidentFullPaths.Values.First();
+
+//            if (entity.Exception == null)
+//                throw new InvalidOperationException(string.Format("Failed to exception in entity: {0}", entity.Id));
+
+//            if (entity.Exception.StackTrace == null)
+//                throw new InvalidOperationException(string.Format("Failed to stack trace in entity: {0}", entity.Id));
+
+//            int pos = entity.Exception.StackTrace.IndexOf("\r\n");
+//            if (pos == -1)
+//                throw new InvalidOperationException(
+//                    string.Format("Failed to find first line in stack trace: {0} for entity {1}",
+//                        entity.Exception.StackTrace, entity.Id));
+
+//            string firstLine = entity.Exception.StackTrace.Substring(0, pos);
+
+//            return IncidentFullPaths[firstLine];
+//        }
+
+//        public void Add(string incidentId, string firstLineInStacktrace)
+//        {
+//            IncidentFullPaths[firstLineInStacktrace] = incidentId;
+//        }
+//    }
+//}
+
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentState.cs b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentState.cs
new file mode 100644
index 00000000..6d853f46
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Incidents/IncidentState.cs
@@ -0,0 +1,34 @@
+namespace Coderr.Server.ReportAnalyzer.Incidents
+{
+    /// 
+    ///     Current state of an incident
+    /// 
+    public enum AnalyzedIncidentState
+    {
+        /// 
+        ///     Incident have arrived but have not yet been categorized.
+        /// 
+        New = 0,
+
+        /// 
+        ///     Incident should be fixed
+        /// 
+        Active = 1,
+
+        /// 
+        ///     Ignore all reports for this incident
+        /// 
+        /// 
+        ///     
+        ///         All inbound reports will be discarded, no notifications will be sent to this incident and it's not show among
+        ///         new or activate incidents
+        ///     
+        /// 
+        Ignored = 2,
+
+        /// 
+        ///     Incident have been corrected.
+        /// 
+        Closed = 3
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/IncludeNonPublicMembersContractResolver.cs b/src/Server/Coderr.Server.ReportAnalyzer/IncludeNonPublicMembersContractResolver.cs
new file mode 100644
index 00000000..24d33026
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/IncludeNonPublicMembersContractResolver.cs
@@ -0,0 +1,42 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
+[assembly:InternalsVisibleTo("Coderr.Server.ReportAnalyzer.Tests")]
+
+namespace Coderr.Server.ReportAnalyzer
+{
+    /// 
+    ///     Allows us to serialize properties with private setters.
+    /// 
+    public class IncludeNonPublicMembersContractResolver : DefaultContractResolver
+    {
+        /// 
+        ///     Creates a  for the given
+        ///     .
+        /// 
+        /// The member's parent .
+        /// The member to create a  for.
+        /// 
+        ///     A created  for the given
+        ///     .
+        /// 
+        protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
+        {
+            //TODO: Maybe cache
+            var prop = base.CreateProperty(member, memberSerialization);
+
+            if (!prop.Writable)
+            {
+                var property = member as PropertyInfo;
+                if (property != null)
+                {
+                    var hasPrivateSetter = property.GetSetMethod(true) != null;
+                    prop.Writable = hasPrivateSetter;
+                }
+            }
+
+            return prop;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Logs/StoreLogEntriesHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Logs/StoreLogEntriesHandler.cs
new file mode 100644
index 00000000..9014d604
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Logs/StoreLogEntriesHandler.cs
@@ -0,0 +1,33 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Modules.Logs;
+using Coderr.Server.ReportAnalyzer.Abstractions.Commands;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.Logs
+{
+    internal class StoreLogEntriesHandler : IMessageHandler
+    {
+        private readonly ILogsRepository _logsRepository;
+
+        public StoreLogEntriesHandler(ILogsRepository logsRepository)
+        {
+            _logsRepository = logsRepository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, StoreLogEntries message)
+        {
+            var entries = message.Entries
+                .Select(x => new LogEntry
+                {
+                    Exception = x.Exception,
+                    Level = (LogLevel) x.Level,
+                    Message = x.Message,
+                    TimeStampUtc = x.TimeStampUtc
+                })
+                .ToList();
+
+            await _logsRepository.Create(message.IncidentId, message.ReportId, entries);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.ReportAnalyzer/Scanners/TryToAnalyzeInvalidReports.cs b/src/Server/Coderr.Server.ReportAnalyzer/Old/Scanners/TryToAnalyzeInvalidReports.cs
similarity index 92%
rename from src/Server/OneTrueError.ReportAnalyzer/Scanners/TryToAnalyzeInvalidReports.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Old/Scanners/TryToAnalyzeInvalidReports.cs
index f1db398f..0e20d19d 100644
--- a/src/Server/OneTrueError.ReportAnalyzer/Scanners/TryToAnalyzeInvalidReports.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Old/Scanners/TryToAnalyzeInvalidReports.cs
@@ -1,139 +1,139 @@
-////TODO: Create MSMQ analyzer
-//using System;
-//using System.Collections.Generic;
-//using System.Configuration;
-//using System.Data;
-//using System.Data.Common;
-//using System.IO;
-//using System.Linq;
-//using System.Threading;
-//using Griffin.ApplicationServices;
-//using Griffin.Container;
-//using Griffin.Data;
-//using log4net;
-//using OneTrueError.MicroService.Core.Authentication;
-//using OneTrueError.ReportAnalyzer.App.Services;
-
-//namespace OneTrueError.ReportAnalyzer.App.Scanners
-//{
-//    [Component(Lifetime = Lifetime.Singleton)]
-//    public class TryToAnalyzeInvalidReports : ApplicationServiceTimer
-//    {
-//        private readonly ReportDtoConverter _reportDtoConverter = new ReportDtoConverter();
-//        private readonly IScopedTaskInvoker _scopedTaskInvoker;
-//        private List _applications = new List();
-//        private ILog _logger = LogManager.GetLogger(typeof (TryToAnalyzeInvalidReports));
-
-//        public TryToAnalyzeInvalidReports(IScopedTaskInvoker scopedTaskInvoker)
-//        {
-//            _scopedTaskInvoker = scopedTaskInvoker;
-//        }
-
-//        protected string BasePath
-//        {
-//            get
-//            {
-//                var path = ConfigurationManager.AppSettings["ReportStoragePath"];
-//                return !string.IsNullOrEmpty(path)
-//                    ? Path.Combine(path, "Failed")
-//                    : @"D:\Logs\InvalidReports\";
-//            }
-//        }
-
-//        private static bool _noAppsWarningIsMade = false;
-
-//        protected override void Execute()
-//        {
-//            var reader = new FileReader();
-//            Dictionary headers;
-//            string json;
-//            LoadApplicationKeys();
-//            if (!_applications.Any())
-//            {
-//                _logger.Error("Failed to find any applications in db.");
-//                _noAppsWarningIsMade = true;
-//                return;
-//            }
-//            _noAppsWarningIsMade = false;
-
-//            while (reader.ReadNextInvalidFile(out headers, out json, FindSharedSecret))
-//            {
-//                var report = _reportDtoConverter.LoadReportFromJson(json);
-
-//                var app = _applications.FirstOrDefault(x => x.ApplicationKey.Equals((string)headers["ApplicationKey"], StringComparison.OrdinalIgnoreCase));
-//                if (app == null)
-//                {
-//                    _logger.Warn("Failed to identify application " + headers["ApplicationKey"]);
-//                    continue;
-//                    //TODO: FAIL
-//                }
-
-//                var newReport = _reportDtoConverter.ConvertReport(report, app.ApplicationId);
-//                if (headers.ContainsKey("RemoteAddress"))
-//                    newReport.RemoteAddress = (string)headers["RemoteAddress"];
-//                var principal = new OneTruePrincipal(app.CustomerId, IAdoNetUnitOfWork.CreateDbName(app.CustomerId));
-//                Thread.CurrentPrincipal = principal;
-//                _scopedTaskInvoker.Execute(x => { x.Analyze(newReport); });
-//            }
-//        }
-
-//        private string FindSharedSecret(string appKey)
-//        {
-//            var app = _applications.FirstOrDefault(x => x.ApplicationKey.Equals(appKey, StringComparison.OrdinalIgnoreCase));
-//            return app == null ? null : app.SharedSecret;
-
-//        }
-
-//        private void LoadApplicationKeys()
-//        {
-//            using (var connection = OpenConnection("ReportsDB"))
-//            using (var transaction = connection.BeginTransaction())
-//            using (var cmd = connection.CreateCommand())
-//            {
-//                cmd.Transaction = transaction;
-//                cmd.CommandText = @"SELECT CustomerId, ApplicationKey, SharedSecret, ApplicationId
-//                                    FROM CustomerApplications";
-
-//                try
-//                {
-//                    using (var reader = cmd.ExecuteReader())
-//                    {
-//                        var applications = new List();
-//                        while (reader.Read())
-//                        {
-//                            var app = new CustomerApp
-//                            {
-//                                ApplicationId = (int)reader["ApplicationId"],
-//                                CustomerId = (int)reader["CustomerId"],
-//                                ApplicationKey = (string)reader["ApplicationKey"],
-//                                SharedSecret = (string)reader["SharedSecret"]
-//                            };
-//                            applications.Add(app);
-//                        }
-//                        _applications = applications;
-//                    }
-//                }
-//                catch (Exception ex)
-//                {
-//                    throw cmd.CreateDataException(ex);
-//                }
-//            }
-//        }
-
-//        private IDbConnection OpenConnection(string name)
-//        {
-//            var conStr = ConfigurationManager.ConnectionStrings[name];
-//            if (conStr == null)
-//                throw new ConfigurationErrorsException("Failed to find connectionString '" + name + "'.");
-//            var provider = DbProviderFactories.GetFactory(conStr.ProviderName);
-//            if (provider == null)
-//                throw new ConfigurationErrorsException("Failed to find DbProviderFactory '" + conStr.ProviderName + "'.");
-
-//            var connection = provider.CreateConnection();
-//            connection.ConnectionString = conStr.ConnectionString;
-//            connection.Open();
-//            return connection;
-//        }
-//    }
-//}
-
+////TODO: Create MSMQ analyzer
+//using System;
+//using System.Collections.Generic;
+//using System.Configuration;
+//using System.Data;
+//using System.Data.Common;
+//using System.IO;
+//using System.Linq;
+//using System.Threading;
+//using Griffin.ApplicationServices;
+//using Coderr.Server.Abstractions.Boot;
+//using Griffin.Data;
+//using log4net;
+//using Coderr.MicroService.Core.Authentication;
+//using Coderr.ReportAnalyzer.App.Services;
+
+//namespace Coderr.ReportAnalyzer.App.Scanners
+//{
+//    [Component(Lifetime = Lifetime.Singleton)]
+//    public class TryToAnalyzeInvalidReports : ApplicationServiceTimer
+//    {
+//        private readonly ReportDtoConverter _reportDtoConverter = new ReportDtoConverter();
+//        private readonly IScopedTaskInvoker _scopedTaskInvoker;
+//        private List _applications = new List();
+//        private ILog _logger = LogManager.GetLogger(typeof (TryToAnalyzeInvalidReports));
+
+//        public TryToAnalyzeInvalidReports(IScopedTaskInvoker scopedTaskInvoker)
+//        {
+//            _scopedTaskInvoker = scopedTaskInvoker;
+//        }
+
+//        protected string BasePath
+//        {
+//            get
+//            {
+//                var path = ConfigurationManager.AppSettings["ReportStoragePath"];
+//                return !string.IsNullOrEmpty(path)
+//                    ? Path.Combine(path, "Failed")
+//                    : @"D:\Logs\InvalidReports\";
+//            }
+//        }
+
+//        private static bool _noAppsWarningIsMade = false;
+
+//        protected override void Execute()
+//        {
+//            var reader = new FileReader();
+//            Dictionary headers;
+//            string json;
+//            LoadApplicationKeys();
+//            if (!_applications.Any())
+//            {
+//                _logger.Error("Failed to find any applications in db.");
+//                _noAppsWarningIsMade = true;
+//                return;
+//            }
+//            _noAppsWarningIsMade = false;
+
+//            while (reader.ReadNextInvalidFile(out headers, out json, FindSharedSecret))
+//            {
+//                var report = _reportDtoConverter.LoadReportFromJson(json);
+
+//                var app = _applications.FirstOrDefault(x => x.ApplicationKey.Equals((string)headers["ApplicationKey"], StringComparison.OrdinalIgnoreCase));
+//                if (app == null)
+//                {
+//                    _logger.Warn("Failed to identify application " + headers["ApplicationKey"]);
+//                    continue;
+//                    //TODO: FAIL
+//                }
+
+//                var newReport = _reportDtoConverter.ConvertReport(report, app.ApplicationId);
+//                if (headers.ContainsKey("RemoteAddress"))
+//                    newReport.RemoteAddress = (string)headers["RemoteAddress"];
+//                var principal = new CoderrPrincipal(app.CustomerId, IAdoNetUnitOfWork.CreateDbName(app.CustomerId));
+//                Thread.CurrentPrincipal = principal;
+//                _scopedTaskInvoker.Execute(x => { x.Analyze(newReport); });
+//            }
+//        }
+
+//        private string FindSharedSecret(string appKey)
+//        {
+//            var app = _applications.FirstOrDefault(x => x.ApplicationKey.Equals(appKey, StringComparison.OrdinalIgnoreCase));
+//            return app == null ? null : app.SharedSecret;
+
+//        }
+
+//        private void LoadApplicationKeys()
+//        {
+//            using (var connection = OpenConnection("ReportsDB"))
+//            using (var transaction = connection.BeginTransaction())
+//            using (var cmd = connection.CreateCommand())
+//            {
+//                cmd.Transaction = transaction;
+//                cmd.CommandText = @"SELECT CustomerId, ApplicationKey, SharedSecret, ApplicationId
+//                                    FROM CustomerApplications";
+
+//                try
+//                {
+//                    using (var reader = cmd.ExecuteReader())
+//                    {
+//                        var applications = new List();
+//                        while (reader.Read())
+//                        {
+//                            var app = new CustomerApp
+//                            {
+//                                ApplicationId = (int)reader["ApplicationId"],
+//                                CustomerId = (int)reader["CustomerId"],
+//                                ApplicationKey = (string)reader["ApplicationKey"],
+//                                SharedSecret = (string)reader["SharedSecret"]
+//                            };
+//                            applications.Add(app);
+//                        }
+//                        _applications = applications;
+//                    }
+//                }
+//                catch (Exception ex)
+//                {
+//                    throw cmd.CreateDataException(ex);
+//                }
+//            }
+//        }
+
+//        private IDbConnection OpenConnection(string name)
+//        {
+//            var conStr = ConfigurationManager.ConnectionStrings[name];
+//            if (conStr == null)
+//                throw new ConfigurationErrorsException("Failed to find connectionString '" + name + "'.");
+//            var provider = DbProviderFactories.GetFactory(conStr.ProviderName);
+//            if (provider == null)
+//                throw new ConfigurationErrorsException("Failed to find DbProviderFactory '" + conStr.ProviderName + "'.");
+
+//            var connection = provider.CreateConnection();
+//            connection.ConnectionString = conStr.ConnectionString;
+//            connection.Open();
+//            return connection;
+//        }
+//    }
+//}
+
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Entities/SpikeAggregation.cs b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Entities/SpikeAggregation.cs
new file mode 100644
index 00000000..a4e59500
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Entities/SpikeAggregation.cs
@@ -0,0 +1,53 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.ReportSpikes.Entities
+{
+    /// 
+    ///     Keeps track of the reports for a specific application.
+    /// 
+    /// 
+    ///     
+    ///         The data itself does not indicate a spike, but are used to detect spikes. Once a spike is detected, we notify
+    ///         the users and then mark the day as completed (notified)
+    ///     
+    /// 
+    public class SpikeAggregation
+    {
+        /// 
+        ///     Application that the report count is for.
+        /// 
+        public int ApplicationId { get; set; }
+
+        public int Id { get; set; }
+
+        /// 
+        ///     we have notified peo
+        /// 
+        public bool Notified { get; set; }
+
+        /// 
+        ///     Number of reports received today.
+        /// 
+        public int ReportCount { get; set; }
+
+        /// 
+        /// 50 percentile for the last 7 days.
+        /// 
+        public int Percentile50 { get; set; }
+
+        /// 
+        /// Name of the application.
+        /// 
+        public string ApplicationName { get; set; }
+
+        /// 
+        /// 85th percentile for the last 7 days.
+        /// 
+        public int Percentile85 { get; set; }
+
+        /// 
+        ///     Date that we are storing the report count for.
+        /// 
+        public DateTime TrackedDate { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Handlers/IncreaseSpikeCountOnReportAdded.cs b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Handlers/IncreaseSpikeCountOnReportAdded.cs
new file mode 100644
index 00000000..d026739b
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Handlers/IncreaseSpikeCountOnReportAdded.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.ReportSpikes.Handlers
+{
+    class IncreaseSpikeCountOnReportAdded : IMessageHandler
+    {
+        private IReportSpikeRepository _repository;
+
+        public IncreaseSpikeCountOnReportAdded(IReportSpikeRepository repository)
+        {
+            _repository = repository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident message)
+        {
+            await _repository.IncreaseReportCount(message.Incident.ApplicationId, message.Report.CreatedAtUtc.Date);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/IReportSpikeRepository.cs b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/IReportSpikeRepository.cs
new file mode 100644
index 00000000..c857b2de
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/IReportSpikeRepository.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.ReportAnalyzer.ReportSpikes.Entities;
+
+namespace Coderr.Server.ReportAnalyzer.ReportSpikes
+{
+    public interface IReportSpikeRepository
+    {
+        Task> GetWeeksAggregations();
+        Task IncreaseReportCount(int applicationId, DateTime when);
+
+        Task MarkAsNotified(int spikeId);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Jobs/FindSpikesToNotifyOn.cs b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Jobs/FindSpikesToNotifyOn.cs
new file mode 100644
index 00000000..f0b08109
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/Jobs/FindSpikesToNotifyOn.cs
@@ -0,0 +1,158 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Client;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Api.Core.Messaging;
+using Coderr.Server.Api.Core.Messaging.Commands;
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Coderr.Server.Infrastructure.Configuration;
+using Coderr.Server.ReportAnalyzer.ReportSpikes.Entities;
+using Coderr.Server.ReportAnalyzer.UserNotifications;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos;
+using DotNetCqs;
+using Griffin.ApplicationServices;
+
+namespace Coderr.Server.ReportAnalyzer.ReportSpikes.Jobs
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class FindSpikesToNotifyOn : IBackgroundJobAsync
+    {
+        private static readonly List _notifiedApplicationsSafetyNet = new List();
+        private static DateTime _safetyNetDate = DateTime.Today;
+        private readonly BaseConfiguration _baseConfiguration;
+        private readonly IMessageBus _messageBus;
+        private readonly INotificationService _notificationService;
+        private readonly IUserNotificationsRepository _notificationsRepository;
+        private readonly IReportSpikeRepository _repository;
+
+        public FindSpikesToNotifyOn(IReportSpikeRepository repository, IConfiguration baseConfiguration,
+            IUserNotificationsRepository notificationsRepository, IMessageBus messageBus,
+            INotificationService notificationService)
+        {
+            _repository = repository;
+            _baseConfiguration = baseConfiguration.Value;
+            _notificationsRepository = notificationsRepository;
+            _messageBus = messageBus;
+            _notificationService = notificationService;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            ClearSafetyNetIfNewDay();
+
+            var aggregations = await _repository.GetWeeksAggregations();
+            var aggregationsPerApplication = aggregations.GroupBy(x => x.ApplicationId);
+            foreach (var appReports in aggregationsPerApplication)
+            {
+                var last = appReports.LastOrDefault();
+                if (last == null || last.TrackedDate != DateTime.Today || last.Notified || last.ReportCount < last.Percentile85)
+                    continue;
+
+                // We have notified of this application recently. No need to do it again.
+                if (appReports.Any(x => x.Notified))
+                    continue;
+
+                if (EnsureThatWeHaventNotifiedForTheApplication(last))
+                    continue;
+
+                await Notify(last);
+
+            }
+        }
+
+        private Notification BuildBrowserNotification(SpikeAggregation spikeInfo, UserNotificationSettings setting)
+        {
+            var notification =
+                new Notification(
+                    $"Coderr have received {spikeInfo.ReportCount} reports so far. Day average is {spikeInfo.Percentile50}.")
+                {
+                    Title = $"Spike detected for {spikeInfo.ApplicationName}",
+                    Actions =
+                        new List
+                        {
+                            new NotificationAction {Title = "View", Action = "discoverApplication"}
+                        },
+                    Data = new
+                    {
+                        applicationId = spikeInfo.ApplicationId,
+                        accountId = setting.AccountId,
+                        discoverApplicationUrlk = $"{_baseConfiguration.BaseUrl}/discover/{spikeInfo.ApplicationId}"
+                    },
+                    Timestamp = DateTime.UtcNow
+                };
+            return notification;
+        }
+
+        private EmailMessage BuildEmail(SpikeAggregation spikeInfo, UserNotificationSettings setting)
+        {
+            var msg = new EmailMessage(setting.AccountId.ToString())
+            {
+                Subject = $"Spike detected for {spikeInfo.ApplicationName} ({spikeInfo} reports)",
+                HtmlBody =
+                    $"We've detected a spike in incoming reports for application {spikeInfo.ApplicationName}\r\n" +
+                    "\r\n" +
+                    $"We've received {spikeInfo.ReportCount} reports so far. Day average is {spikeInfo.Percentile50}\r\n" +
+                    "\r\n" + "No further notifications will be sent today for this application."
+            };
+            return msg;
+        }
+
+        private static void ClearSafetyNetIfNewDay()
+        {
+            if (_safetyNetDate.Date == DateTime.Today)
+            {
+                return;
+            }
+
+            _notifiedApplicationsSafetyNet.Clear();
+            _safetyNetDate = DateTime.Today;
+        }
+
+        private static bool EnsureThatWeHaventNotifiedForTheApplication(SpikeAggregation violation)
+        {
+            if (_notifiedApplicationsSafetyNet.Contains(violation.ApplicationId)) return true;
+
+            _notifiedApplicationsSafetyNet.Add(violation.ApplicationId);
+            return false;
+        }
+
+        private async Task Notify(SpikeAggregation spikeAggregation)
+        {
+            var notificationSettings =
+                (await _notificationsRepository.GetAllAsync(spikeAggregation.ApplicationId)).ToList();
+            if (notificationSettings.All(x => x.ApplicationSpike == NotificationState.Disabled))
+                return;
+
+            spikeAggregation.Notified = true;
+            await _repository.MarkAsNotified(spikeAggregation.Id);
+
+            foreach (var setting in notificationSettings)
+            {
+                if (setting.ApplicationSpike == NotificationState.Disabled)
+                    continue;
+
+                if (setting.ApplicationSpike == NotificationState.Email)
+                {
+                    var msg = BuildEmail(spikeAggregation, setting);
+                    var cmd = new SendEmail(msg);
+                    await _messageBus.SendAsync(cmd);
+                }
+                else
+                {
+                    var notification = BuildBrowserNotification(spikeAggregation, setting);
+                    try
+                    {
+                        await _notificationService.SendBrowserNotification(setting.AccountId, notification);
+                    }
+                    catch (Exception ex)
+                    {
+                        Err.Report(ex, new {setting, notification});
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/NewSpike.cs b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/NewSpike.cs
new file mode 100644
index 00000000..8fc474cb
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ReportSpikes/NewSpike.cs
@@ -0,0 +1,21 @@
+namespace Coderr.Server.ReportAnalyzer.ReportSpikes
+{
+    /// 
+    ///     Detected a new spike
+    /// 
+    public class NewSpike
+    {
+        public int ApplicationId { get; set; }
+        /// 
+        ///     Typical report count average per day
+        /// 
+        public int DayAverage { get; set; }
+
+        public string ApplicationName { get; set; }
+
+        /// 
+        ///     Current report count
+        /// 
+        public int SpikeCount { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/ReportsExtensions.cs b/src/Server/Coderr.Server.ReportAnalyzer/ReportsExtensions.cs
new file mode 100644
index 00000000..80233365
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/ReportsExtensions.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using System.Linq;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer
+{
+    public static class ReportsExtensions
+    {
+        public static ErrorReportContextCollection GetCoderrCollection(
+            this IEnumerable instance)
+        {
+            return instance.FirstOrDefault(x => x.Name == "CoderrData");
+        }
+
+        public static ErrorReportContextCollection GetCoderrCollection(
+            this ErrorReportEntity instance)
+        {
+            return instance.ContextCollections.FirstOrDefault(x => x.Name == "CoderrData");
+        }
+
+
+        public static ErrorReportContextCollection FindCollection(this ErrorReportEntity instance, string collectionName)
+        {
+            return instance.ContextCollections.FirstOrDefault(x => x.Name == collectionName);
+        }
+
+        public static string FindCollectionProperty(this ErrorReportEntity instance, string collectionName, string propertyName)
+        {
+            var collection = instance.FindCollection(collectionName);
+            if (collection == null)
+            {
+                return null;
+            }
+
+            if (collection.Properties.TryGetValue(propertyName, out var value))
+            {
+                return value;
+            }
+            else
+            {
+                return null;
+            }
+        }
+
+
+        public static ContextCollectionDTO FindCollection(this ReportDTO instance, string collectionName)
+        {
+            return instance.ContextCollections.FirstOrDefault(x => x.Name == collectionName);
+        }
+
+        public static string FindCollectionProperty(this ReportDTO instance, string collectionName, string propertyName)
+        {
+            var collection = instance.FindCollection(collectionName);
+            if (collection == null)
+            {
+                return null;
+            }
+
+            if (collection.Properties.TryGetValue(propertyName, out var value))
+            {
+                return value;
+            }
+            else
+            {
+                return null;
+            }
+        }
+
+        public static string FindCollectionProperty(this ReportDTO instance, string propertyName)
+        {
+            foreach (var collection in instance.ContextCollections)
+            {
+                if (collection.Properties.TryGetValue(propertyName, out var value))
+                {
+                    return value;
+                }
+                
+            }
+
+            return null;
+        }
+
+
+    }
+}
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/ApplicationInfoAdapter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ApplicationInfoAdapter.cs
similarity index 90%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/ApplicationInfoAdapter.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ApplicationInfoAdapter.cs
index 17d8c921..88229c86 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/ApplicationInfoAdapter.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ApplicationInfoAdapter.cs
@@ -1,71 +1,70 @@
-using System;
-using System.Linq;
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.Normalizers;
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters
-{
-    /// 
-    ///     Generates the "ApplicationInfo" context collection from information in different uploaded collections.
-    /// 
-    public class ApplicationInfoAdapter : IValueAdapter
-    {
-        private static readonly string[] MemoryProperties = {"WorkingSet", "VirtualMemorySize", "PrivateMemorySize"};
-
-        /// 
-        ///     Adapt the value specified in the context
-        /// 
-        /// Context information
-        /// Value which might have been adapted
-        /// The new value (or same as the current value if no modification has been made)
-        public object Adapt(ValueAdapterContext context, object currentValue)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            if (currentValue == null || context.ContextName != "ApplicationInfo")
-                return currentValue;
-
-            if (context.PropertyName == "MainModule")
-            {
-                if (currentValue.ToString().StartsWith("System.Diagnostics.ProcessModule"))
-                    return currentValue.ToString()
-                        .Substring("System.Diagnostics.ProcessModule".Length)
-                        .Trim(' ', '(', ')');
-            }
-
-            if (MemoryProperties.Any(x => x.Equals(context.PropertyName, StringComparison.OrdinalIgnoreCase)))
-            {
-                var value = 0;
-                if (!int.TryParse(currentValue.ToString(), out value))
-                {
-                    return currentValue;
-                }
-
-                value = value/1000000;
-                return MemoryNormalizer.Divide(value, context.TypeOfApplication == "Mobile" ? 32 : 512);
-            }
-
-            if (context.PropertyName == "ThreadCount")
-            {
-                return NumberNormalizer.Normalize(currentValue.ToString(), 10, 50);
-            }
-            if (context.PropertyName == "HandleCount")
-            {
-                return NumberNormalizer.Normalize(currentValue.ToString(), 100, 5000);
-            }
-
-            if (context.PropertyName == "StartTime")
-            {
-                DateTime dt;
-                if (!DateTime.TryParse(currentValue.ToString(), out dt))
-                    return currentValue;
-
-                context.IgnoreProperty = true;
-                context.AddCustomField("ApplicationInfo", "StartTime.Hour", dt.Hour);
-                context.AddCustomField("ApplicationInfo", "StartTime.DayOfWeek", dt.DayOfWeek.ToString());
-            }
-
-            context.IgnoreProperty = true;
-            return currentValue;
-        }
-    }
+using System;
+using System.Linq;
+using Coderr.Server.ReportAnalyzer.Similarities.Adapters.Normalizers;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    /// 
+    ///     Generates the "ApplicationInfo" context collection from information in different uploaded collections.
+    /// 
+    public class ApplicationInfoAdapter : IValueAdapter
+    {
+        private static readonly string[] MemoryProperties = {"WorkingSet", "VirtualMemorySize", "PrivateMemorySize"};
+
+        /// 
+        ///     Adapt the value specified in the context
+        /// 
+        /// Context information
+        /// Value which might have been adapted
+        /// The new value (or same as the current value if no modification has been made)
+        public object Adapt(ValueAdapterContext context, object currentValue)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            if (currentValue == null || context.ContextName != "ApplicationInfo")
+                return currentValue;
+
+            if (context.PropertyName == "MainModule")
+            {
+                if (currentValue.ToString().StartsWith("System.Diagnostics.ProcessModule"))
+                    return currentValue.ToString()
+                        .Substring("System.Diagnostics.ProcessModule".Length)
+                        .Trim(' ', '(', ')');
+            }
+
+            if (MemoryProperties.Any(x => x.Equals(context.PropertyName, StringComparison.OrdinalIgnoreCase)))
+            {
+                var value = 0;
+                if (!int.TryParse(currentValue.ToString(), out value))
+                {
+                    return currentValue;
+                }
+
+                value = value/1000000;
+                return MemoryNormalizer.Divide(value, context.TypeOfApplication == "Mobile" ? 32 : 512);
+            }
+
+            if (context.PropertyName == "ThreadCount")
+            {
+                return NumberNormalizer.Normalize(currentValue.ToString(), 10, 50);
+            }
+            if (context.PropertyName == "HandleCount")
+            {
+                return NumberNormalizer.Normalize(currentValue.ToString(), 100, 5000);
+            }
+
+            if (context.PropertyName == "StartTime")
+            {
+                DateTime dt;
+                if (!DateTime.TryParse(currentValue.ToString(), out dt))
+                    return currentValue;
+
+                context.IgnoreProperty = true;
+                context.AddCustomField("ApplicationInfo", "StartTime.Hour", dt.Hour);
+                context.AddCustomField("ApplicationInfo", "StartTime.DayOfWeek", dt.DayOfWeek.ToString());
+            }
+
+            context.IgnoreProperty = true;
+            return currentValue;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/CustomField.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/CustomField.cs
similarity index 93%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/CustomField.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/CustomField.cs
index b5028780..b555ca11 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/CustomField.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/CustomField.cs
@@ -1,46 +1,46 @@
-using System;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters
-{
-    /// 
-    ///     Custom fields are added by the value adapters.
-    /// 
-    /// 
-    ///     
-    ///         For instance the UserAgent string can be parsed into different custom fields like OS, Browser, OS etc.
-    ///     
-    /// 
-    public class CustomField
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// Context collection that the new field belongs in.
-        /// Property name, like "BrowserVersion"
-        /// Actual value
-        public CustomField(string contextName, string propertyName, object value)
-        {
-            if (contextName == null) throw new ArgumentNullException("contextName");
-            if (propertyName == null) throw new ArgumentNullException("propertyName");
-            if (value == null) throw new ArgumentNullException("value");
-            ContextName = contextName;
-            PropertyName = propertyName;
-            Value = value;
-        }
-
-        /// 
-        ///     Collection that the field belongs in
-        /// 
-        public string ContextName { get; set; }
-
-        /// 
-        ///     Property name like "BrowserVersion"
-        /// 
-        public string PropertyName { get; set; }
-
-        /// 
-        ///     Value
-        /// 
-        public object Value { get; set; }
-    }
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    /// 
+    ///     Custom fields are added by the value adapters.
+    /// 
+    /// 
+    ///     
+    ///         For instance the UserAgent string can be parsed into different custom fields like OS, Browser, OS etc.
+    ///     
+    /// 
+    public class CustomField
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Context collection that the new field belongs in.
+        /// Property name, like "BrowserVersion"
+        /// Actual value
+        public CustomField(string contextName, string propertyName, object value)
+        {
+            if (contextName == null) throw new ArgumentNullException("contextName");
+            if (propertyName == null) throw new ArgumentNullException("propertyName");
+            if (value == null) throw new ArgumentNullException("value");
+            ContextName = contextName;
+            PropertyName = propertyName;
+            Value = value;
+        }
+
+        /// 
+        ///     Collection that the field belongs in
+        /// 
+        public string ContextName { get; set; }
+
+        /// 
+        ///     Property name like "BrowserVersion"
+        /// 
+        public string PropertyName { get; set; }
+
+        /// 
+        ///     Value
+        /// 
+        public object Value { get; set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/ExceptionPropertiesAdapter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ExceptionPropertiesAdapter.cs
similarity index 84%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/ExceptionPropertiesAdapter.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ExceptionPropertiesAdapter.cs
index 84203384..c66cf202 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/ExceptionPropertiesAdapter.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ExceptionPropertiesAdapter.cs
@@ -1,30 +1,29 @@
-using System;
-using System.Linq;
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters
-{
-    internal class ExceptionPropertiesAdapter : IValueAdapter
-    {
-        private readonly string[] IgnoredProperties =
-        {
-            "Message", "StackTrace", "InnerException", "TargetSite.",
-            "TargetSite"
-        };
-
-        public object Adapt(ValueAdapterContext context, object currentValue)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            if (!context.ContextName.Equals("ExceptionProperties", StringComparison.OrdinalIgnoreCase))
-                return currentValue;
-
-            if (IgnoredProperties.Any(x => x.Equals(context.PropertyName, StringComparison.OrdinalIgnoreCase)))
-            {
-                context.IgnoreProperty = true;
-                return currentValue;
-            }
-
-            return currentValue;
-        }
-    }
+using System;
+using System.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    internal class ExceptionPropertiesAdapter : IValueAdapter
+    {
+        private readonly string[] IgnoredProperties =
+        {
+            "Message", "StackTrace", "InnerException", "TargetSite.",
+            "TargetSite"
+        };
+
+        public object Adapt(ValueAdapterContext context, object currentValue)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            if (!context.ContextName.Equals("ExceptionProperties", StringComparison.OrdinalIgnoreCase))
+                return currentValue;
+
+            if (IgnoredProperties.Any(x => x.Equals(context.PropertyName, StringComparison.OrdinalIgnoreCase)))
+            {
+                context.IgnoreProperty = true;
+                return currentValue;
+            }
+
+            return currentValue;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/IAdapterRepository.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/IAdapterRepository.cs
similarity index 88%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/IAdapterRepository.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/IAdapterRepository.cs
index 0d92c912..3edeff8d 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/IAdapterRepository.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/IAdapterRepository.cs
@@ -1,20 +1,20 @@
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters
-{
-    /// 
-    ///     Keeps track of all adapters which are used to normalize the incoming values to be able to find similarities.
-    /// 
-    /// 
-    /// 
-    public interface IAdapterRepository
-    {
-        /// 
-        ///     Get a list of all value adapters.
-        /// 
-        /// All identified adapters (single instances)
-        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
-        IReadOnlyList GetAdapters();
-    }
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    /// 
+    ///     Keeps track of all adapters which are used to normalize the incoming values to be able to find similarities.
+    /// 
+    /// 
+    /// 
+    public interface IAdapterRepository
+    {
+        /// 
+        ///     Get a list of all value adapters.
+        /// 
+        /// All identified adapters (single instances)
+        [SuppressMessage("Microsoft.Design", "CA1024:UsePropertiesWhereAppropriate")]
+        IReadOnlyList GetAdapters();
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/IValueAdapter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/IValueAdapter.cs
similarity index 82%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/IValueAdapter.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/IValueAdapter.cs
index 315ec979..51cb1790 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/IValueAdapter.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/IValueAdapter.cs
@@ -1,21 +1,19 @@
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters
-{
-    /// 
-    ///     Values might need to be normalized in some way before the similarities are calculated.
-    /// 
-    /// 
-    ///     For instance the amount of used memory needs to be rounded off into blocks.
-    /// 
-    public interface IValueAdapter
-    {
-        /// 
-        ///     Adapt the value specified in the context
-        /// 
-        /// Context information
-        /// Value which might have been adapted
-        /// The new value (or same as the current value if no modification has been made)
-        object Adapt(ValueAdapterContext context, object currentValue);
-    }
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    /// 
+    ///     Values might need to be normalized in some way before the similarities are calculated.
+    /// 
+    /// 
+    ///     For instance the amount of used memory needs to be rounded off into blocks.
+    /// 
+    public interface IValueAdapter
+    {
+        /// 
+        ///     Adapt the value specified in the context
+        /// 
+        /// Context information
+        /// Value which might have been adapted
+        /// The new value (or same as the current value if no modification has been made)
+        object Adapt(ValueAdapterContext context, object currentValue);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/NamespaceDoc.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/NamespaceDoc.cs
new file mode 100644
index 00000000..31acdf90
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/NamespaceDoc.cs
@@ -0,0 +1,14 @@
+using System.Runtime.CompilerServices;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    /// 
+    ///     För att kunna identifiera likheter behöver absoluta värden att normaliseras. Exempelvis säger inte "483 handles"
+    ///     någonting, men om man grupperar i > 10, > 100, > 1000, > 10000 så säger det mer.
+    ///     Notera att grundvärdena kan man fortfarande få genom att titta på varje incident.
+    /// 
+    [CompilerGenerated]
+    internal class NamespaceDoc
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Normalizers/MemoryNormalizer.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Normalizers/MemoryNormalizer.cs
similarity index 92%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Normalizers/MemoryNormalizer.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Normalizers/MemoryNormalizer.cs
index 1e5c39fa..3ea36935 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Normalizers/MemoryNormalizer.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Normalizers/MemoryNormalizer.cs
@@ -1,45 +1,45 @@
-using System;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters.Normalizers
-{
-    /// 
-    ///     Divides memory usage into ranges,
-    /// 
-    public static class MemoryNormalizer
-    {
-        /// 
-        ///     Divide using a predefined segmentation size.
-        /// 
-        /// Memory usage
-        /// segment size in Mb
-        /// memory usage like "Range 10-50Mb"
-        public static string Divide(string memoryInMb, float segmentSize)
-        {
-            int value;
-            return !int.TryParse(memoryInMb, out value)
-                ? null
-                : Divide(value, segmentSize);
-        }
-
-        /// 
-        ///     Divide using a predefined segmentation size.
-        /// 
-        /// Memory usage
-        /// segment size in Mb
-        /// memory usage like "Range 10-50Mb"
-        public static string Divide(int memoryInMb, float segmentSize)
-        {
-            if (memoryInMb < 10)
-                return "Range 0-10 Mb";
-            if (memoryInMb < 50)
-                return "Range 10-50 Mb";
-            if (memoryInMb < 100)
-                return "Range 50-100 Mb";
-            if (memoryInMb > 2000)
-                return "> 2Gb";
-
-            var lowerPart = (long) Math.Floor(memoryInMb/segmentSize)*segmentSize;
-            return "Range " + lowerPart + "-" + (lowerPart + segmentSize) + " Mb";
-        }
-    }
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters.Normalizers
+{
+    /// 
+    ///     Divides memory usage into ranges,
+    /// 
+    public static class MemoryNormalizer
+    {
+        /// 
+        ///     Divide using a predefined segmentation size.
+        /// 
+        /// Memory usage
+        /// segment size in Mb
+        /// memory usage like "Range 10-50Mb"
+        public static string Divide(string memoryInMb, float segmentSize)
+        {
+            int value;
+            return !int.TryParse(memoryInMb, out value)
+                ? null
+                : Divide(value, segmentSize);
+        }
+
+        /// 
+        ///     Divide using a predefined segmentation size.
+        /// 
+        /// Memory usage
+        /// segment size in Mb
+        /// memory usage like "Range 10-50Mb"
+        public static string Divide(int memoryInMb, float segmentSize)
+        {
+            if (memoryInMb < 10)
+                return "Range 0-10 Mb";
+            if (memoryInMb < 50)
+                return "Range 10-50 Mb";
+            if (memoryInMb < 100)
+                return "Range 50-100 Mb";
+            if (memoryInMb > 2000)
+                return "> 2Gb";
+
+            var lowerPart = (long) Math.Floor(memoryInMb/segmentSize)*segmentSize;
+            return "Range " + lowerPart + "-" + (lowerPart + segmentSize) + " Mb";
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Normalizers/NumberNormalizer.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Normalizers/NumberNormalizer.cs
similarity index 86%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Normalizers/NumberNormalizer.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Normalizers/NumberNormalizer.cs
index f385d4ad..59aa8667 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Normalizers/NumberNormalizer.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Normalizers/NumberNormalizer.cs
@@ -1,26 +1,26 @@
-using System;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters.Normalizers
-{
-    internal class NumberNormalizer
-    {
-        public static string Normalize(string value, float step, int max)
-        {
-            if (value == null)
-                return null;
-
-            var value2 = 0;
-            if (!int.TryParse(value, out value2))
-                return null;
-
-            if (value2 < 10)
-                return "Less than 10";
-
-            if (value2 >= max)
-                return "> " + max;
-
-            var lowerValue = (int) Math.Floor(value2/step)*(int) step;
-            return "Between " + lowerValue + " and " + (lowerValue + (int) step);
-        }
-    }
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters.Normalizers
+{
+    internal class NumberNormalizer
+    {
+        public static string Normalize(string value, float step, int max)
+        {
+            if (value == null)
+                return null;
+
+            var value2 = 0;
+            if (!int.TryParse(value, out value2))
+                return null;
+
+            if (value2 < 10)
+                return "Less than 10";
+
+            if (value2 >= max)
+                return "> " + max;
+
+            var lowerValue = (int) Math.Floor(value2/step)*(int) step;
+            return "Between " + lowerValue + " and " + (lowerValue + (int) step);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Old/ProcessorTimeAdapter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Old/ProcessorTimeAdapter.cs
similarity index 89%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Old/ProcessorTimeAdapter.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Old/ProcessorTimeAdapter.cs
index 49f45843..a96789dd 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Old/ProcessorTimeAdapter.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Old/ProcessorTimeAdapter.cs
@@ -1,44 +1,43 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters.Old
-{
-    internal class ProcessorTimeAdapter : IValueAdapter
-    {
-        private static readonly string[] Fields = {"TotalProcessorTime", "UserProcessorTime"};
-
-        public object Adapt(ValueAdapterContext context, object currentValue)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-
-            if (context.ContextName != "ApplicationInfo")
-                return currentValue;
-
-            if (!Fields.Contains(context.PropertyName, EqualityComparer.Default))
-                return currentValue;
-
-            var timeSpan = TimeSpan.Parse(context.Value.ToString());
-
-            if (timeSpan < TimeSpan.FromMinutes(1))
-                return "under one minute";
-            if (timeSpan < TimeSpan.FromMinutes(30))
-                return "between 1 and 30 minutes";
-            if (timeSpan < TimeSpan.FromHours(1))
-                return "between 30 minutes and an hour";
-            if (timeSpan < TimeSpan.FromDays(1))
-                return "between an hour and a day";
-            if (timeSpan < TimeSpan.FromDays(30))
-                return "up to a month";
-            if (timeSpan < TimeSpan.FromDays(90))
-                return "between one and three months";
-            if (timeSpan > TimeSpan.FromDays(365/2))
-                return "between three and six months";
-            if (timeSpan > TimeSpan.FromDays(365/2))
-                return "between 6 months and a year";
-
-            return "up to " + (int) timeSpan.TotalDays/365 + " years";
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters.Old
+{
+    internal class ProcessorTimeAdapter : IValueAdapter
+    {
+        private static readonly string[] Fields = {"TotalProcessorTime", "UserProcessorTime"};
+
+        public object Adapt(ValueAdapterContext context, object currentValue)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+
+            if (context.ContextName != "ApplicationInfo")
+                return currentValue;
+
+            if (!Fields.Contains(context.PropertyName, EqualityComparer.Default))
+                return currentValue;
+
+            var timeSpan = TimeSpan.Parse(context.Value.ToString());
+
+            if (timeSpan < TimeSpan.FromMinutes(1))
+                return "under one minute";
+            if (timeSpan < TimeSpan.FromMinutes(30))
+                return "between 1 and 30 minutes";
+            if (timeSpan < TimeSpan.FromHours(1))
+                return "between 30 minutes and an hour";
+            if (timeSpan < TimeSpan.FromDays(1))
+                return "between an hour and a day";
+            if (timeSpan < TimeSpan.FromDays(30))
+                return "up to a month";
+            if (timeSpan < TimeSpan.FromDays(90))
+                return "between one and three months";
+            if (timeSpan > TimeSpan.FromDays(365/2))
+                return "between three and six months";
+            if (timeSpan > TimeSpan.FromDays(365/2))
+                return "between 6 months and a year";
+
+            return "up to " + (int) timeSpan.TotalDays/365 + " years";
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystemAdapter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystemAdapter.cs
similarity index 95%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystemAdapter.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystemAdapter.cs
index 7b65714f..e641e268 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystemAdapter.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystemAdapter.cs
@@ -1,289 +1,289 @@
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Globalization;
-using System.Linq;
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.Normalizers;
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.OperatingSystems;
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters
-{
-    /// 
-    ///     Converts the Operating system WMI collection into more useful information.
-    /// 
-    /// TODO: Document which collections this one generates.
-    //http://www.powertheshell.com/reference/wmireference/root/cimv2/win32_operatingsystem/
-    public class OperatingSystemAdapter : IValueAdapter
-    {
-        private static readonly string[] LocalizationProperties =
-        {
-            "CurrentTimeZone",
-            "CountryCode",
-            "MUILanguages", "CodeSet", "Locale", "OSLanguage"
-        };
-
-        private static readonly string[] OsEnvironment =
-        {
-            "NumberOfUsers", "SizeStoredInPagingFiles", "SystemDevice", "SystemDirectory", "SystemDrive",
-            "TotalVirtualMemorySize", "TotalVisibleMemorySize", "WindowsDirectory", "NumberOfProcesses", "NumberOfUsers",
-            "FreePhysicalMemory", "FreeSpaceInPagingFiles", "FreeVirtualMemory"
-        };
-
-        //private string[] _allowedProperties =
-        //{
-        //    "Primary", "PortableOperatingSystem", "Version",
-        //    "Caption", "OSArchitecture", "BuildNumber", "BuildType"
-        //};
-
-        //TODO: Refactor code below into multiple methods and use the dictionary
-        //to invoke the correct property handler.
-
-        /// 
-        ///     Adapt the value specified in the context
-        /// 
-        /// Context information
-        /// Value which might have been adapted
-        /// The new value (or same as the current value if no modification has been made)
-        [SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength",
-            Justification = "Value cannot be null.")]
-        public object Adapt(ValueAdapterContext context, object currentValue)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            var val = currentValue as string;
-            if (string.IsNullOrEmpty(val))
-                return currentValue;
-            if (!context.ContextName.Equals("OperatingSystem", StringComparison.OrdinalIgnoreCase))
-                return currentValue;
-
-            context.IgnoreProperty = true;
-            if (context.PropertyName.Equals("OperatingSystemSKU", StringComparison.OrdinalIgnoreCase))
-            {
-                var name = OperatingSystemSku.GetName(currentValue.ToString()) ?? currentValue;
-                context.AddCustomField("OS.Metadata", "Edition", name);
-            }
-
-            if (context.PropertyName.Equals("CSDVersion", StringComparison.OrdinalIgnoreCase))
-            {
-                context.AddCustomField("OS.Metadata", "ServicePack", currentValue.ToString());
-                return currentValue;
-            }
-
-            if (context.PropertyName.Equals("OSProductSuite", StringComparison.OrdinalIgnoreCase))
-            {
-                var value = OsProductSuite.GetNames(currentValue.ToString()) ?? currentValue;
-                context.AddCustomField("OS.Metadata", "ProductSuite", value);
-                return currentValue;
-            }
-
-            if (context.PropertyName.Equals("ProductType", StringComparison.OrdinalIgnoreCase))
-            {
-                switch (currentValue.ToString())
-                {
-                    case "1":
-                        currentValue = "Work Station";
-                        break;
-                    case "2":
-                        currentValue = "Domain Controller";
-                        break;
-                    case "3":
-                        currentValue = "Server";
-                        break;
-                }
-                context.AddCustomField("OS.Metadata", "ProductType", currentValue);
-                return currentValue;
-            }
-
-
-            if (context.PropertyName.Equals("SuiteMask", StringComparison.OrdinalIgnoreCase))
-            {
-                var bitMask = 0;
-                var value = "";
-                if (int.TryParse(currentValue.ToString(), out bitMask))
-                {
-                    if ((bitMask & 1) != 0)
-                        value += "Small Business, ";
-                    if ((bitMask & 2) != 0)
-                        value += "Enterprise, ";
-                    if ((bitMask & 4) != 0)
-                        value += "BackOffice, ";
-                    if ((bitMask & 8) != 0)
-                        value += "Communications, ";
-                    if ((bitMask & 16) != 0)
-                        value += "Terminal Services, ";
-                    if ((bitMask & 32) != 0)
-                        value += "Small Business Restricted, ";
-                    if ((bitMask & 64) != 0)
-                        value += "Embedded Edition, ";
-                    if ((bitMask & 128) != 0)
-                        value += "Datacenter Edition, ";
-                    if ((bitMask & 256) != 0)
-                        value += "Single User, ";
-                    if ((bitMask & 512) != 0)
-                        value += "Home Edition, ";
-                    if ((bitMask & 1024) != 0)
-                        value += "Web Server Edition, ";
-                }
-                if (value != "")
-                    currentValue = value.Remove(value.Length - 2, 2);
-                context.AddCustomField("OS.Metadata", "Suite2", currentValue);
-                return currentValue;
-            }
-            if (context.PropertyName.Equals("QuantumLength", StringComparison.OrdinalIgnoreCase))
-            {
-                switch (currentValue.ToString())
-                {
-                    case "0":
-                        currentValue = "Unknown ";
-                        break;
-                    case "1":
-                        currentValue = "One tick";
-                        break;
-                    case "2":
-                        currentValue = "Two ticks";
-                        break;
-                }
-                context.AddCustomField("OS.Metadata", "QuantumLength", currentValue);
-                return currentValue;
-            }
-
-            if (context.PropertyName.Equals("QuantumType", StringComparison.OrdinalIgnoreCase))
-            {
-                switch (currentValue.ToString())
-                {
-                    case "0":
-                        currentValue = "Unknown ";
-                        break;
-                    case "1":
-                        currentValue = "Fixed";
-                        break;
-                    case "2":
-                        currentValue = "Variable";
-                        break;
-                }
-                context.AddCustomField("OS.Metadata", "QuantumType", currentValue);
-                return currentValue;
-            }
-
-
-            if (context.PropertyName == "InstallDate")
-            {
-                return AdaptInstallDate(context, currentValue);
-            }
-
-
-            if (context.PropertyName == "LastBootUpTime")
-            {
-                DateTime time;
-                if (WmiDateConverter.TryParse(context.Value.ToString(), out time))
-                {
-                    context.AddCustomField("OS.Environment", "LastBootup.Hour", time.Hour);
-                    context.AddCustomField("OS.Environment", "LastBootup.DayOfWeek", time.DayOfWeek.ToString());
-                }
-            }
-
-
-            if (context.PropertyName == "LocalDateTime")
-            {
-                return AdaptLocalTime(context, currentValue);
-            }
-
-            if (context.PropertyName.StartsWith("Free", StringComparison.OrdinalIgnoreCase) ||
-                context.PropertyName.StartsWith("Total", StringComparison.OrdinalIgnoreCase) ||
-                context.PropertyName.Equals("SizeStoredInPagingFiles", StringComparison.OrdinalIgnoreCase))
-            {
-                var divisor = context.TypeOfApplication == "Server" ? 512 : 256;
-                var value = MemoryNormalizer.Divide(currentValue as string, divisor);
-                if (!string.IsNullOrEmpty(value))
-                    context.AddCustomField("OS.Environment", context.PropertyName, value);
-
-                return currentValue;
-            }
-
-            //allow as-is
-            if (context.PropertyName.StartsWith("DataExecutionPrevention", StringComparison.OrdinalIgnoreCase))
-            {
-                context.AddCustomField("OS.Metadata", context.PropertyName, currentValue);
-                return currentValue;
-            }
-
-            if (LocalizationProperties.Any(x => x.Equals(context.PropertyName, StringComparison.OrdinalIgnoreCase)))
-            {
-                if (context.PropertyName == "Locale")
-                {
-                    int lcid;
-                    if (int.TryParse(currentValue.ToString(), NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo,
-                        out lcid))
-                    {
-                        currentValue = CultureInfo.GetCultureInfo(lcid).Name;
-                    }
-                }
-
-                context.AddCustomField("OS.Localization", context.PropertyName, currentValue);
-                return null;
-            }
-
-            if (OsEnvironment.Any(x => x.Equals(context.PropertyName, StringComparison.OrdinalIgnoreCase)))
-            {
-                if (context.PropertyName == "NumberOfUsers")
-                {
-                    if ("0".Equals(currentValue))
-                        currentValue = "0";
-                    else if ("1".Equals(currentValue))
-                        currentValue = "1";
-                    else
-                        currentValue = "> 1";
-                    context.AddCustomField("OS.Environment", "NumberOfLoggedInUsers", currentValue);
-                    return currentValue;
-                }
-
-                if (context.PropertyName == "NumberOfProcesses")
-                {
-                    currentValue = NumberNormalizer.Normalize(currentValue as string, 20, 1000);
-                }
-
-                context.AddCustomField("OS.Environment", context.PropertyName, currentValue);
-                return currentValue;
-            }
-
-            return currentValue;
-        }
-
-        private static object AdaptInstallDate(ValueAdapterContext context, object currentValue)
-        {
-            DateTime time;
-            if (WmiDateConverter.TryParse(context.Value.ToString(), out time))
-            {
-                context.AddCustomField("OS.Environment", "InstallDate.Year", time.Year.ToString());
-                context.AddCustomField("OS.Environment", "InstallDate.YearMonth", time.ToString("yyyy-MM"));
-            }
-
-            context.IgnoreProperty = true;
-            return currentValue;
-        }
-
-        private static object AdaptLocalTime(ValueAdapterContext context, object currentValue)
-        {
-            var val = context.Value.ToString();
-            var pos = val.IndexOfAny(new[] {'-', '+'});
-            if (pos != -1)
-            {
-                var pos2 = val.IndexOfAny(new[] {'-', ' '}, pos + 1);
-                if (pos2 == -1)
-                    context.AddCustomField("OS.Localization", "LocalDateTime.TimeZone", val.Substring(pos));
-            }
-
-
-            DateTime time;
-            if (WmiDateConverter.TryParse(context.Value.ToString(), out time))
-            {
-                context.AddCustomField("OS.Localization", "LocalDateTime.Hour", time.Hour.ToString());
-                context.AddCustomField("OS.Localization", "LocalDateTime.Minute", time.Minute.ToString());
-                context.AddCustomField("OS.Localization", "LocalDateTime.DayOfWeek", time.DayOfWeek.ToString());
-                context.AddCustomField("OS.Localization", "LocalDateTime.DayOfYear", time.DayOfYear);
-            }
-
-            context.IgnoreProperty = true;
-            return currentValue;
-        }
-    }
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using Coderr.Server.ReportAnalyzer.Similarities.Adapters.Normalizers;
+using Coderr.Server.ReportAnalyzer.Similarities.Adapters.OperatingSystems;
+using Coderr.Server.ReportAnalyzer.Similarities.Handlers.Processing;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    /// 
+    ///     Converts the Operating system WMI collection into more useful information.
+    /// 
+    /// TODO: Document which collections this one generates.
+    //http://www.powertheshell.com/reference/wmireference/root/cimv2/win32_operatingsystem/
+    public class OperatingSystemAdapter : IValueAdapter
+    {
+        private static readonly string[] LocalizationProperties =
+        {
+            "CurrentTimeZone",
+            "CountryCode",
+            "MUILanguages", "CodeSet", "Locale", "OSLanguage"
+        };
+
+        private static readonly string[] OsEnvironment =
+        {
+            "NumberOfUsers", "SizeStoredInPagingFiles", "SystemDevice", "SystemDirectory", "SystemDrive",
+            "TotalVirtualMemorySize", "TotalVisibleMemorySize", "WindowsDirectory", "NumberOfProcesses", "NumberOfUsers",
+            "FreePhysicalMemory", "FreeSpaceInPagingFiles", "FreeVirtualMemory"
+        };
+
+        //private string[] _allowedProperties =
+        //{
+        //    "Primary", "PortableOperatingSystem", "Version",
+        //    "Caption", "OSArchitecture", "BuildNumber", "BuildType"
+        //};
+
+        //TODO: Refactor code below into multiple methods and use the dictionary
+        //to invoke the correct property handler.
+
+        /// 
+        ///     Adapt the value specified in the context
+        /// 
+        /// Context information
+        /// Value which might have been adapted
+        /// The new value (or same as the current value if no modification has been made)
+        [SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength",
+            Justification = "Value cannot be null.")]
+        public object Adapt(ValueAdapterContext context, object currentValue)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            var val = currentValue as string;
+            if (string.IsNullOrEmpty(val))
+                return currentValue;
+            if (!context.ContextName.Equals("OperatingSystem", StringComparison.OrdinalIgnoreCase))
+                return currentValue;
+
+            context.IgnoreProperty = true;
+            if (context.PropertyName.Equals("OperatingSystemSKU", StringComparison.OrdinalIgnoreCase))
+            {
+                var name = OperatingSystemSku.GetName(currentValue.ToString()) ?? currentValue;
+                context.AddCustomField("OS.Metadata", "Edition", name);
+            }
+
+            if (context.PropertyName.Equals("CSDVersion", StringComparison.OrdinalIgnoreCase))
+            {
+                context.AddCustomField("OS.Metadata", "ServicePack", currentValue.ToString());
+                return currentValue;
+            }
+
+            if (context.PropertyName.Equals("OSProductSuite", StringComparison.OrdinalIgnoreCase))
+            {
+                var value = OsProductSuite.GetNames(currentValue.ToString()) ?? currentValue;
+                context.AddCustomField("OS.Metadata", "ProductSuite", value);
+                return currentValue;
+            }
+
+            if (context.PropertyName.Equals("ProductType", StringComparison.OrdinalIgnoreCase))
+            {
+                switch (currentValue.ToString())
+                {
+                    case "1":
+                        currentValue = "Work Station";
+                        break;
+                    case "2":
+                        currentValue = "Domain Controller";
+                        break;
+                    case "3":
+                        currentValue = "Server";
+                        break;
+                }
+                context.AddCustomField("OS.Metadata", "ProductType", currentValue);
+                return currentValue;
+            }
+
+
+            if (context.PropertyName.Equals("SuiteMask", StringComparison.OrdinalIgnoreCase))
+            {
+                var bitMask = 0;
+                var value = "";
+                if (int.TryParse(currentValue.ToString(), out bitMask))
+                {
+                    if ((bitMask & 1) != 0)
+                        value += "Small Business, ";
+                    if ((bitMask & 2) != 0)
+                        value += "Enterprise, ";
+                    if ((bitMask & 4) != 0)
+                        value += "BackOffice, ";
+                    if ((bitMask & 8) != 0)
+                        value += "Communications, ";
+                    if ((bitMask & 16) != 0)
+                        value += "Terminal Services, ";
+                    if ((bitMask & 32) != 0)
+                        value += "Small Business Restricted, ";
+                    if ((bitMask & 64) != 0)
+                        value += "Embedded Edition, ";
+                    if ((bitMask & 128) != 0)
+                        value += "Datacenter Edition, ";
+                    if ((bitMask & 256) != 0)
+                        value += "Single User, ";
+                    if ((bitMask & 512) != 0)
+                        value += "Home Edition, ";
+                    if ((bitMask & 1024) != 0)
+                        value += "Web Server Edition, ";
+                }
+                if (value != "")
+                    currentValue = value.Remove(value.Length - 2, 2);
+                context.AddCustomField("OS.Metadata", "Suite2", currentValue);
+                return currentValue;
+            }
+            if (context.PropertyName.Equals("QuantumLength", StringComparison.OrdinalIgnoreCase))
+            {
+                switch (currentValue.ToString())
+                {
+                    case "0":
+                        currentValue = "Unknown ";
+                        break;
+                    case "1":
+                        currentValue = "One tick";
+                        break;
+                    case "2":
+                        currentValue = "Two ticks";
+                        break;
+                }
+                context.AddCustomField("OS.Metadata", "QuantumLength", currentValue);
+                return currentValue;
+            }
+
+            if (context.PropertyName.Equals("QuantumType", StringComparison.OrdinalIgnoreCase))
+            {
+                switch (currentValue.ToString())
+                {
+                    case "0":
+                        currentValue = "Unknown ";
+                        break;
+                    case "1":
+                        currentValue = "Fixed";
+                        break;
+                    case "2":
+                        currentValue = "Variable";
+                        break;
+                }
+                context.AddCustomField("OS.Metadata", "QuantumType", currentValue);
+                return currentValue;
+            }
+
+
+            if (context.PropertyName == "InstallDate")
+            {
+                return AdaptInstallDate(context, currentValue);
+            }
+
+
+            if (context.PropertyName == "LastBootUpTime")
+            {
+                DateTime time;
+                if (WmiDateConverter.TryParse(context.Value.ToString(), out time))
+                {
+                    context.AddCustomField("OS.Environment", "LastBootup.Hour", time.Hour);
+                    context.AddCustomField("OS.Environment", "LastBootup.DayOfWeek", time.DayOfWeek.ToString());
+                }
+            }
+
+
+            if (context.PropertyName == "LocalDateTime")
+            {
+                return AdaptLocalTime(context, currentValue);
+            }
+
+            if (context.PropertyName.StartsWith("Free", StringComparison.OrdinalIgnoreCase) ||
+                context.PropertyName.StartsWith("Total", StringComparison.OrdinalIgnoreCase) ||
+                context.PropertyName.Equals("SizeStoredInPagingFiles", StringComparison.OrdinalIgnoreCase))
+            {
+                var divisor = context.TypeOfApplication == "Server" ? 512 : 256;
+                var value = MemoryNormalizer.Divide(currentValue as string, divisor);
+                if (!string.IsNullOrEmpty(value))
+                    context.AddCustomField("OS.Environment", context.PropertyName, value);
+
+                return currentValue;
+            }
+
+            //allow as-is
+            if (context.PropertyName.StartsWith("DataExecutionPrevention", StringComparison.OrdinalIgnoreCase))
+            {
+                context.AddCustomField("OS.Metadata", context.PropertyName, currentValue);
+                return currentValue;
+            }
+
+            if (LocalizationProperties.Any(x => x.Equals(context.PropertyName, StringComparison.OrdinalIgnoreCase)))
+            {
+                if (context.PropertyName == "Locale")
+                {
+                    int lcid;
+                    if (int.TryParse(currentValue.ToString(), NumberStyles.HexNumber, NumberFormatInfo.InvariantInfo,
+                        out lcid))
+                    {
+                        currentValue = CultureInfo.GetCultureInfo(lcid).Name;
+                    }
+                }
+
+                context.AddCustomField("OS.Localization", context.PropertyName, currentValue);
+                return null;
+            }
+
+            if (OsEnvironment.Any(x => x.Equals(context.PropertyName, StringComparison.OrdinalIgnoreCase)))
+            {
+                if (context.PropertyName == "NumberOfUsers")
+                {
+                    if ("0".Equals(currentValue))
+                        currentValue = "0";
+                    else if ("1".Equals(currentValue))
+                        currentValue = "1";
+                    else
+                        currentValue = "> 1";
+                    context.AddCustomField("OS.Environment", "NumberOfLoggedInUsers", currentValue);
+                    return currentValue;
+                }
+
+                if (context.PropertyName == "NumberOfProcesses")
+                {
+                    currentValue = NumberNormalizer.Normalize(currentValue as string, 20, 1000);
+                }
+
+                context.AddCustomField("OS.Environment", context.PropertyName, currentValue);
+                return currentValue;
+            }
+
+            return currentValue;
+        }
+
+        private static object AdaptInstallDate(ValueAdapterContext context, object currentValue)
+        {
+            DateTime time;
+            if (WmiDateConverter.TryParse(context.Value.ToString(), out time))
+            {
+                context.AddCustomField("OS.Environment", "InstallDate.Year", time.Year.ToString());
+                context.AddCustomField("OS.Environment", "InstallDate.YearMonth", time.ToString("yyyy-MM"));
+            }
+
+            context.IgnoreProperty = true;
+            return currentValue;
+        }
+
+        private static object AdaptLocalTime(ValueAdapterContext context, object currentValue)
+        {
+            var val = context.Value.ToString();
+            var pos = val.IndexOfAny(new[] {'-', '+'});
+            if (pos != -1)
+            {
+                var pos2 = val.IndexOfAny(new[] {'-', ' '}, pos + 1);
+                if (pos2 == -1)
+                    context.AddCustomField("OS.Localization", "LocalDateTime.TimeZone", val.Substring(pos));
+            }
+
+
+            DateTime time;
+            if (WmiDateConverter.TryParse(context.Value.ToString(), out time))
+            {
+                context.AddCustomField("OS.Localization", "LocalDateTime.Hour", time.Hour.ToString());
+                context.AddCustomField("OS.Localization", "LocalDateTime.Minute", time.Minute.ToString());
+                context.AddCustomField("OS.Localization", "LocalDateTime.DayOfWeek", time.DayOfWeek.ToString());
+                context.AddCustomField("OS.Localization", "LocalDateTime.DayOfYear", time.DayOfYear);
+            }
+
+            context.IgnoreProperty = true;
+            return currentValue;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystems/OperatingSystemSku.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystems/OperatingSystemSku.cs
similarity index 95%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystems/OperatingSystemSku.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystems/OperatingSystemSku.cs
index bdbec38a..edc85ff8 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystems/OperatingSystemSku.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystems/OperatingSystemSku.cs
@@ -1,110 +1,110 @@
-using System.Collections.Generic;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters.OperatingSystems
-{
-    /// 
-    ///     Translates the WMI collection named "OperatingSystemSKU"
-    /// 
-    public static class OperatingSystemSku
-    {
-        private static readonly Dictionary Editions = new Dictionary
-        {
-            {1, "Ultimate"},
-            {2, "Home Basic"},
-            {3, "Home Premium"},
-            {4, "Ent"},
-            {5, "Home Basic N"},
-            {6, "Business"},
-            {7, "Server Std"},
-            {8, "Server DC (full)"},
-            {9, "Windows SBS"},
-            {10, "Server Ent (full)"},
-            {11, "Starter"},
-            {12, "Server DC (core)"},
-            {13, "Server Std (core)"},
-            {14, "Server Ent (core)"},
-            {15, "Server Ent for Itanium-based Systems"},
-            {16, "Business N"},
-            {17, "Web Server (full)"},
-            {18, "HPC Edition"},
-            {19, "Windows Storage Server 2008 R2 Essentials"},
-            {20, "Storage Server Express"},
-            {21, "Storage Server Std"},
-            {22, "Storage Server Workgroup"},
-            {23, "Storage Server Ent"},
-            {24, "Windows Server 2008 for Windows Essential Server Solutions"},
-            {25, "SBS Premium"},
-            {26, "Home Premium N"},
-            {27, "Enterprise N"},
-            {28, "Ultimate N"},
-            {29, "Web Server (core)"},
-            {30, "Windows Essential Business Server Management Server"},
-            {31, "Windows Essential Business Server Security Server"},
-            {32, "Windows Essential Business Server Messaging Server"},
-            {33, "Server Foundation"},
-            {34, "Windows Home Server 2011"},
-            {35, "Windows Server 2008 w/o Hyper-V for Windows Essential Server Solutions"},
-            {36, "Server Std w/o Hyper-V"},
-            {37, "Server DC w/o Hyper-V (full)"},
-            {38, "Server Ent w/o Hyper-V (full)"},
-            {39, "Server DC w/o Hyper-V (core)"},
-            {40, "Server Std w/o Hyper-V (core)"},
-            {41, "Server Ent w/o Hyper-V (core)"},
-            {42, "Microsoft Hyper-V Server"},
-            {43, "Storage Server Express (core)"},
-            {44, "Storage Server Std (core)"},
-            {45, "Storage Server Workgroup (core)"},
-            {46, "Storage Server Ent (core)"},
-            {47, "Starter N"},
-            {48, "Professional"},
-            {49, "Professional N"},
-            {50, "Windows SBS 2011 Essentials"},
-            {51, "Server For SB Solutions"},
-            {52, "Server Solutions Premium"},
-            {53, "Server Solutions Premium (core)"},
-            {54, "Server For SB Solutions EM"},
-            {55, "Server For SB Solutions EM"},
-            {56, "Windows MultiPoint Server"},
-            {59, "Windows Essential Server Solution Management"},
-            {60, "Windows Essential Server Solution Additional"},
-            {61, "Windows Essential Server Solution Management SVC"},
-            {62, "Windows Essential Server Solution Additional SVC"},
-            {63, "SBS Premium (core)"},
-            {64, "Server Hyper Core V"},
-            {66, "Starter E"},
-            {67, "Home Basic E"},
-            {68, "Home Premium E"},
-            {69, "Professional E"},
-            {70, "Enterprise E"},
-            {71, "Ultimate E"},
-            {72, "Server Ent (evaluation)"},
-            {76, "Windows MultiPoint Server Std (full)"},
-            {77, "Windows MultiPoint Server Premium (full)"},
-            {79, "Server Std (evaluation)"},
-            {80, "Server DC (evaluation)"},
-            {84, "Enterprise N (evaluation)"},
-            {95, "Storage Server Workgroup (evaluation)"},
-            {96, "Storage Server Std (evaluation)"},
-            {98, "Windows 8 N"},
-            {99, "Windows 8 China"},
-            {100, "Windows 8 Single Language"},
-            {101, "Windows 8"},
-            {103, "Professional with Media Center"}
-        };
-
-        /// 
-        ///     Get edition from a suite index
-        /// 
-        /// index
-        /// corresponding suite if found; otherwise "Unknown"
-        public static string GetName(string id)
-        {
-            int idInt;
-            if (!int.TryParse(id, out idInt))
-                return "Unknown";
-
-            string item;
-            return Editions.TryGetValue(idInt, out item) ? item : "Unknown";
-        }
-    }
+using System.Collections.Generic;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters.OperatingSystems
+{
+    /// 
+    ///     Translates the WMI collection named "OperatingSystemSKU"
+    /// 
+    public static class OperatingSystemSku
+    {
+        private static readonly Dictionary Editions = new Dictionary
+        {
+            {1, "Ultimate"},
+            {2, "Home Basic"},
+            {3, "Home Premium"},
+            {4, "Ent"},
+            {5, "Home Basic N"},
+            {6, "Business"},
+            {7, "Server Std"},
+            {8, "Server DC (full)"},
+            {9, "Windows SBS"},
+            {10, "Server Ent (full)"},
+            {11, "Starter"},
+            {12, "Server DC (core)"},
+            {13, "Server Std (core)"},
+            {14, "Server Ent (core)"},
+            {15, "Server Ent for Itanium-based Systems"},
+            {16, "Business N"},
+            {17, "Web Server (full)"},
+            {18, "HPC Edition"},
+            {19, "Windows Storage Server 2008 R2 Essentials"},
+            {20, "Storage Server Express"},
+            {21, "Storage Server Std"},
+            {22, "Storage Server Workgroup"},
+            {23, "Storage Server Ent"},
+            {24, "Windows Server 2008 for Windows Essential Server Solutions"},
+            {25, "SBS Premium"},
+            {26, "Home Premium N"},
+            {27, "Enterprise N"},
+            {28, "Ultimate N"},
+            {29, "Web Server (core)"},
+            {30, "Windows Essential Business Server Management Server"},
+            {31, "Windows Essential Business Server Security Server"},
+            {32, "Windows Essential Business Server Messaging Server"},
+            {33, "Server Foundation"},
+            {34, "Windows Home Server 2011"},
+            {35, "Windows Server 2008 w/o Hyper-V for Windows Essential Server Solutions"},
+            {36, "Server Std w/o Hyper-V"},
+            {37, "Server DC w/o Hyper-V (full)"},
+            {38, "Server Ent w/o Hyper-V (full)"},
+            {39, "Server DC w/o Hyper-V (core)"},
+            {40, "Server Std w/o Hyper-V (core)"},
+            {41, "Server Ent w/o Hyper-V (core)"},
+            {42, "Microsoft Hyper-V Server"},
+            {43, "Storage Server Express (core)"},
+            {44, "Storage Server Std (core)"},
+            {45, "Storage Server Workgroup (core)"},
+            {46, "Storage Server Ent (core)"},
+            {47, "Starter N"},
+            {48, "Professional"},
+            {49, "Professional N"},
+            {50, "Windows SBS 2011 Essentials"},
+            {51, "Server For SB Solutions"},
+            {52, "Server Solutions Premium"},
+            {53, "Server Solutions Premium (core)"},
+            {54, "Server For SB Solutions EM"},
+            {55, "Server For SB Solutions EM"},
+            {56, "Windows MultiPoint Server"},
+            {59, "Windows Essential Server Solution Management"},
+            {60, "Windows Essential Server Solution Additional"},
+            {61, "Windows Essential Server Solution Management SVC"},
+            {62, "Windows Essential Server Solution Additional SVC"},
+            {63, "SBS Premium (core)"},
+            {64, "Server Hyper Core V"},
+            {66, "Starter E"},
+            {67, "Home Basic E"},
+            {68, "Home Premium E"},
+            {69, "Professional E"},
+            {70, "Enterprise E"},
+            {71, "Ultimate E"},
+            {72, "Server Ent (evaluation)"},
+            {76, "Windows MultiPoint Server Std (full)"},
+            {77, "Windows MultiPoint Server Premium (full)"},
+            {79, "Server Std (evaluation)"},
+            {80, "Server DC (evaluation)"},
+            {84, "Enterprise N (evaluation)"},
+            {95, "Storage Server Workgroup (evaluation)"},
+            {96, "Storage Server Std (evaluation)"},
+            {98, "Windows 8 N"},
+            {99, "Windows 8 China"},
+            {100, "Windows 8 Single Language"},
+            {101, "Windows 8"},
+            {103, "Professional with Media Center"}
+        };
+
+        /// 
+        ///     Get edition from a suite index
+        /// 
+        /// index
+        /// corresponding suite if found; otherwise "Unknown"
+        public static string GetName(string id)
+        {
+            int idInt;
+            if (!int.TryParse(id, out idInt))
+                return "Unknown";
+
+            string item;
+            return Editions.TryGetValue(idInt, out item) ? item : "Unknown";
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystems/OsProductSuite.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystems/OsProductSuite.cs
similarity index 93%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystems/OsProductSuite.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystems/OsProductSuite.cs
index a64af7ee..79404720 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/OperatingSystems/OsProductSuite.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/OperatingSystems/OsProductSuite.cs
@@ -1,58 +1,58 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters.OperatingSystems
-{
-    /// 
-    ///     Translates the WMI collection "OS_PRODUCT_SUITE"
-    /// 
-    [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Os")]
-    public static class OsProductSuite
-    {
-        private static readonly Dictionary SuiteTypes = new Dictionary
-        {
-            {1, "SmallBusiness"},
-            {2, "Enterprise"},
-            {4, "BackOffice"},
-            {8, "CommunicationServer"},
-            {16, "TerminalServer"},
-            {32, "SmallBusinessRestricted"},
-            {64, "EmbeddedNT"},
-            {128, "DataCenter"},
-            {256, "Terminal Services"},
-            {512, "Windows Home"},
-            {1024, "Web Server"},
-            {8192, "Storage Server"},
-            {16384, "Compute Cluster"}
-        };
-
-        /// 
-        ///     Get names from suite flags.
-        /// 
-        /// flag value
-        /// Matching suites delimited by new lines.
-        [SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength",
-            Justification = "Value cannot be null.")]
-        public static string GetNames(string suitValues)
-        {
-            if (suitValues == null) throw new ArgumentNullException("suitValues");
-
-            int chosenSuits;
-            if (!int.TryParse(suitValues, out chosenSuits))
-                return "";
-
-            var result = "";
-            foreach (var item1 in SuiteTypes)
-            {
-                if ((chosenSuits & item1.Key) != 0)
-                    result += item1.Value + "\r\n";
-            }
-
-            if (result != "")
-                result = result.Substring(result.Length - 2, 2);
-
-            return result;
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters.OperatingSystems
+{
+    /// 
+    ///     Translates the WMI collection "OS_PRODUCT_SUITE"
+    /// 
+    [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Os")]
+    public static class OsProductSuite
+    {
+        private static readonly Dictionary SuiteTypes = new Dictionary
+        {
+            {1, "SmallBusiness"},
+            {2, "Enterprise"},
+            {4, "BackOffice"},
+            {8, "CommunicationServer"},
+            {16, "TerminalServer"},
+            {32, "SmallBusinessRestricted"},
+            {64, "EmbeddedNT"},
+            {128, "DataCenter"},
+            {256, "Terminal Services"},
+            {512, "Windows Home"},
+            {1024, "Web Server"},
+            {8192, "Storage Server"},
+            {16384, "Compute Cluster"}
+        };
+
+        /// 
+        ///     Get names from suite flags.
+        /// 
+        /// flag value
+        /// Matching suites delimited by new lines.
+        [SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength",
+            Justification = "Value cannot be null.")]
+        public static string GetNames(string suitValues)
+        {
+            if (suitValues == null) throw new ArgumentNullException("suitValues");
+
+            int chosenSuits;
+            if (!int.TryParse(suitValues, out chosenSuits))
+                return "";
+
+            var result = "";
+            foreach (var item1 in SuiteTypes)
+            {
+                if ((chosenSuits & item1.Key) != 0)
+                    result += item1.Value + "\r\n";
+            }
+
+            if (result != "")
+                result = result.Substring(result.Length - 2, 2);
+
+            return result;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Runner/AdapterRepository.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Runner/AdapterRepository.cs
similarity index 91%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Runner/AdapterRepository.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Runner/AdapterRepository.cs
index cf3c4e29..4de28d73 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Runner/AdapterRepository.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/Runner/AdapterRepository.cs
@@ -1,36 +1,36 @@
-using System;
-using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Reflection;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner
-{
-    /// 
-    ///     Loads similarity adapters by using reflection.
-    /// 
-    public class AdapterRepository : IAdapterRepository
-    {
-        private static readonly List Adapters;
-
-        [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline",
-            Justification = "How would I do that?")]
-        static AdapterRepository()
-        {
-            Adapters = (from type in Assembly.GetExecutingAssembly().GetTypes()
-                where type.IsClass
-                      && !type.IsAbstract
-                      && typeof(IValueAdapter).IsAssignableFrom(type)
-                select type).ToList();
-        }
-
-        /// 
-        ///     Get a list of all value adapters.
-        /// 
-        /// All identified adapters (single instances)
-        public IReadOnlyList GetAdapters()
-        {
-            return Adapters.Select(Activator.CreateInstance).Cast().ToList();
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters.Runner
+{
+    /// 
+    ///     Loads similarity adapters by using reflection.
+    /// 
+    public class AdapterRepository : IAdapterRepository
+    {
+        private static readonly List Adapters;
+
+        [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline",
+            Justification = "How would I do that?")]
+        static AdapterRepository()
+        {
+            Adapters = (from type in Assembly.GetExecutingAssembly().GetTypes()
+                where type.IsClass
+                      && !type.IsAbstract
+                      && typeof(IValueAdapter).IsAssignableFrom(type)
+                select type).ToList();
+        }
+
+        /// 
+        ///     Get a list of all value adapters.
+        /// 
+        /// All identified adapters (single instances)
+        public IReadOnlyList GetAdapters()
+        {
+            return Adapters.Select(Activator.CreateInstance).Cast().ToList();
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/UserAgentAdapter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/UserAgentAdapter.cs
similarity index 81%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/UserAgentAdapter.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/UserAgentAdapter.cs
index c59c70e3..99ece746 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/UserAgentAdapter.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/UserAgentAdapter.cs
@@ -1,42 +1,41 @@
-using System;
-using OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner;
-using UAParser;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters
-{
-    /// 
-    ///     Splits a user agent string into multiple context properties.
-    /// 
-    public class UserAgentAdapter : IValueAdapter
-    {
-        /// 
-        ///     Adapt the value specified in the context
-        /// 
-        /// Context information
-        /// Value which might have been adapted
-        /// The new value (or same as the current value if no modification has been made)
-        public object Adapt(ValueAdapterContext context, object currentValue)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-
-            if (context.ContextName != "HttpHeaders" || context.PropertyName != "User-Agent" || context.Value == null)
-                return currentValue;
-
-            var uaParser = Parser.GetDefault();
-            var c = uaParser.Parse(context.Value.ToString());
-            context.AddCustomField("UserAgent.Family", c.UserAgent.Family);
-            context.AddCustomField("UserAgent.Version",
-                string.Format("{0} v{1}.{2}", c.UserAgent.Family, c.UserAgent.Major, c.UserAgent.Minor));
-
-
-            context.AddCustomField("UserInfo", "DeviceType", c.Device.Family);
-            context.AddCustomField("UserInfo", "IsWebSpider", c.Device.IsSpider);
-            context.AddCustomField("UserInfo", "OS.Family", c.OS.Family);
-            context.AddCustomField("UserInfo", "OS.Version", c.OS.Major + "." + c.OS.Minor);
-            context.AddCustomField("UserInfo", "OS.VersionPatch", c.OS.Patch + "." + c.OS.PatchMinor);
-
-            context.IgnoreProperty = true;
-            return currentValue;
-        }
-    }
+using System;
+using UAParser;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    /// 
+    ///     Splits a user agent string into multiple context properties.
+    /// 
+    public class UserAgentAdapter : IValueAdapter
+    {
+        /// 
+        ///     Adapt the value specified in the context
+        /// 
+        /// Context information
+        /// Value which might have been adapted
+        /// The new value (or same as the current value if no modification has been made)
+        public object Adapt(ValueAdapterContext context, object currentValue)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+
+            if (context.ContextName != "HttpHeaders" || context.PropertyName != "User-Agent" || context.Value == null)
+                return currentValue;
+
+            var uaParser = Parser.GetDefault();
+            var c = uaParser.Parse(context.Value.ToString());
+            context.AddCustomField("UserAgent.Family", c.UA.Family);
+            context.AddCustomField("UserAgent.Version",
+                string.Format("{0} v{1}.{2}", c.UA.Family, c.UA.Major, c.UA.Minor));
+
+
+            context.AddCustomField("UserInfo", "DeviceType", c.Device.Family);
+            context.AddCustomField("UserInfo", "IsWebSpider", c.Device.IsSpider);
+            context.AddCustomField("UserInfo", "OS.Family", c.OS.Family);
+            context.AddCustomField("UserInfo", "OS.Version", c.OS.Major + "." + c.OS.Minor);
+            context.AddCustomField("UserInfo", "OS.VersionPatch", c.OS.Patch + "." + c.OS.PatchMinor);
+
+            context.IgnoreProperty = true;
+            return currentValue;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Runner/ValueAdapterContext.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ValueAdapterContext.cs
similarity index 95%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Runner/ValueAdapterContext.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ValueAdapterContext.cs
index aa8d565d..3f380f35 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/Runner/ValueAdapterContext.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Adapters/ValueAdapterContext.cs
@@ -1,114 +1,114 @@
-using System;
-using System.Collections.Generic;
-using OneTrueError.Api.Core.Reports;
-
-namespace OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner
-{
-    /// 
-    ///     Context for .
-    /// 
-    public class ValueAdapterContext
-    {
-        private readonly List _customFields = new List();
-
-        /// 
-        ///     Initializes a new instance of the  class.
-        /// 
-        /// Context being processed.
-        /// Name of the property that the value is for.
-        /// The value. Can be null as null can be specified during the collection.
-        /// Entire report which the context/property belongs to.
-        public ValueAdapterContext(string contextName, string propertyName, object value, ReportDTO report)
-        {
-            if (contextName == null) throw new ArgumentNullException("contextName");
-            if (propertyName == null) throw new ArgumentNullException("propertyName");
-            if (report == null) throw new ArgumentNullException("report");
-
-            Report = report;
-            Value = value;
-            PropertyName = propertyName;
-            ContextName = contextName;
-
-            //TODO: Load real application type.
-            TypeOfApplication = "DesktopApplication";
-        }
-
-        /// 
-        ///     Context being processed.
-        /// 
-        public string ContextName { get; private set; }
-
-        /// 
-        ///     New fields created by the adapters
-        /// 
-        public IEnumerable CustomFields
-        {
-            get { return _customFields; }
-        }
-
-        /// 
-        ///     Tells the analyzer that this specific property should not be added.
-        /// 
-        public bool IgnoreProperty { get; set; }
-
-        /// 
-        ///     Property name in the context collection
-        /// 
-        public string PropertyName { get; private set; }
-
-        /// 
-        ///     Report that the context collection is for.
-        /// 
-        public ReportDTO Report { get; private set; }
-
-        /// 
-        ///     DesktopApplication, Mobile, Server. From the  enum.
-        /// 
-        public string TypeOfApplication { get; set; }
-
-        /// 
-        ///     Property value
-        /// 
-        public object Value { get; private set; }
-
-        /// 
-        ///     Add a new custom field
-        /// 
-        /// Name of the new field
-        /// Property value for the new field
-        /// 
-        ///     
-        ///         (used when the property value is aggregated information that should be split into multiple fields.
-        ///     
-        /// 
-        /// propertyName; value
-        public void AddCustomField(string propertyName, object value)
-        {
-            if (propertyName == null) throw new ArgumentNullException("propertyName");
-            if (value == null) throw new ArgumentNullException("value");
-
-            _customFields.Add(new CustomField(ContextName, propertyName, value));
-        }
-
-        /// 
-        ///     Add a new custom field
-        /// 
-        /// Associate the property with another context collection
-        /// Name of the new field
-        /// Property value for the new field
-        /// 
-        ///     
-        ///         (used when the property value is aggregated information that should be split into multiple fields.
-        ///     
-        /// 
-        /// contextName; propertyName; value
-        public void AddCustomField(string contextName, string propertyName, object value)
-        {
-            if (contextName == null) throw new ArgumentNullException("contextName");
-            if (propertyName == null) throw new ArgumentNullException("propertyName");
-            if (value == null) throw new ArgumentNullException("value");
-
-            _customFields.Add(new CustomField(contextName, propertyName, value));
-        }
-    }
+using System;
+using System.Collections.Generic;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Adapters
+{
+    /// 
+    ///     Context for .
+    /// 
+    public class ValueAdapterContext
+    {
+        private readonly List _customFields = new List();
+
+        /// 
+        ///     Initializes a new instance of the  class.
+        /// 
+        /// Context being processed.
+        /// Name of the property that the value is for.
+        /// The value. Can be null as null can be specified during the collection.
+        /// Entire report which the context/property belongs to.
+        public ValueAdapterContext(string contextName, string propertyName, object value, ReportDTO report)
+        {
+            if (contextName == null) throw new ArgumentNullException("contextName");
+            if (propertyName == null) throw new ArgumentNullException("propertyName");
+            if (report == null) throw new ArgumentNullException("report");
+
+            Report = report;
+            Value = value;
+            PropertyName = propertyName;
+            ContextName = contextName;
+
+            //TODO: Load real application type.
+            TypeOfApplication = "DesktopApplication";
+        }
+
+        /// 
+        ///     Context being processed.
+        /// 
+        public string ContextName { get; private set; }
+
+        /// 
+        ///     New fields created by the adapters
+        /// 
+        public IEnumerable CustomFields
+        {
+            get { return _customFields; }
+        }
+
+        /// 
+        ///     Tells the analyzer that this specific property should not be added.
+        /// 
+        public bool IgnoreProperty { get; set; }
+
+        /// 
+        ///     Property name in the context collection
+        /// 
+        public string PropertyName { get; private set; }
+
+        /// 
+        ///     Report that the context collection is for.
+        /// 
+        public ReportDTO Report { get; private set; }
+
+        /// 
+        ///     DesktopApplication, Mobile, Server. From the  enum.
+        /// 
+        public string TypeOfApplication { get; set; }
+
+        /// 
+        ///     Property value
+        /// 
+        public object Value { get; private set; }
+
+        /// 
+        ///     Add a new custom field
+        /// 
+        /// Name of the new field
+        /// Property value for the new field
+        /// 
+        ///     
+        ///         (used when the property value is aggregated information that should be split into multiple fields.
+        ///     
+        /// 
+        /// propertyName; value
+        public void AddCustomField(string propertyName, object value)
+        {
+            if (propertyName == null) throw new ArgumentNullException("propertyName");
+            if (value == null) throw new ArgumentNullException("value");
+
+            _customFields.Add(new CustomField(ContextName, propertyName, value));
+        }
+
+        /// 
+        ///     Add a new custom field
+        /// 
+        /// Associate the property with another context collection
+        /// Name of the new field
+        /// Property value for the new field
+        /// 
+        ///     
+        ///         (used when the property value is aggregated information that should be split into multiple fields.
+        ///     
+        /// 
+        /// contextName; propertyName; value
+        public void AddCustomField(string contextName, string propertyName, object value)
+        {
+            if (contextName == null) throw new ArgumentNullException("contextName");
+            if (propertyName == null) throw new ArgumentNullException("propertyName");
+            if (value == null) throw new ArgumentNullException("value");
+
+            _customFields.Add(new CustomField(contextName, propertyName, value));
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/DeleteAbandonedSimilarities.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/DeleteAbandonedSimilarities.cs
new file mode 100644
index 00000000..bb0316ee
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/DeleteAbandonedSimilarities.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class DeleteAbandonedSimilarities : IBackgroundJobAsync
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private static DateTime _executionDate = DateTime.Today;
+
+        public DeleteAbandonedSimilarities(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            if (_executionDate == DateTime.Today)
+                return;
+
+            _executionDate = DateTime.Today;
+
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"DELETE FROM IncidentCorrelations
+                                    FROM IncidentCorrelations
+                                    LEFT JOIN Incidents ON (Incidents.Id = IncidentId)
+                                    WHERE Incidents.Id IS NULL";
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/ISimilarityRepository.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/ISimilarityRepository.cs
similarity index 88%
rename from src/Server/OneTrueError.App/Modules/Similarities/Domain/ISimilarityRepository.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/ISimilarityRepository.cs
index e41c2863..4e00ab29 100644
--- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/ISimilarityRepository.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/ISimilarityRepository.cs
@@ -1,30 +1,31 @@
-using System.Threading.Tasks;
-
-namespace OneTrueError.App.Modules.Similarities.Domain
-{
-    /// 
-    ///     Store and fetch all similarities in the database
-    /// 
-    public interface ISimilarityRepository
-    {
-        /// 
-        ///     Create a similarity report (one time per incident)
-        /// 
-        /// report
-        /// task
-        Task CreateAsync(SimilaritiesReport similarity);
-
-        /// 
-        /// 
-        /// 
-        /// Report if found; otherwise null
-        SimilaritiesReport FindForIncident(int incidentId);
-
-        /// 
-        ///     Update existing report
-        /// 
-        /// similarity report
-        /// task
-        Task UpdateAsync(SimilaritiesReport similarity);
-    }
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Modules.Similarities;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Handlers.Processing
+{
+    /// 
+    ///     Store and fetch all similarities in the database
+    /// 
+    public interface ISimilarityRepository
+    {
+        /// 
+        ///     Create a similarity report (one time per incident)
+        /// 
+        /// report
+        /// task
+        Task CreateAsync(SimilaritiesReport similarity);
+
+        /// 
+        /// 
+        /// 
+        /// Report if found; otherwise null
+        SimilaritiesReport FindForIncident(int incidentId);
+
+        /// 
+        ///     Update existing report
+        /// 
+        /// similarity report
+        /// task
+        Task UpdateAsync(SimilaritiesReport similarity);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/NamespaceDoc.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/NamespaceDoc.cs
new file mode 100644
index 00000000..85ee7bf5
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/NamespaceDoc.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Handlers.Processing
+{
+    /// 
+    ///     Similarities are used to analyze every error report to detect what all reports for an incident have in common.
+    /// 
+    /// 
+    ///     This is great if you get an exception on just some of the client computers.
+    /// 
+    [CompilerGenerated]
+    internal class NamespaceDoc
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/WmiDateConverter.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/WmiDateConverter.cs
new file mode 100644
index 00000000..fa0a8521
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/Processing/WmiDateConverter.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Handlers.Processing
+{
+    /// 
+    ///     Translates WMI dates to .NETs DateTime.
+    /// 
+    public static class WmiDateConverter
+    {
+        private static readonly ILog _logger = LogManager.GetLogger(typeof(WmiDateConverter));
+
+        /// 
+        ///     Try parse a WMI date
+        /// 
+        /// date
+        /// converted date
+        /// true if successful; otherwise false.
+        /// date
+        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
+    public static bool TryParse(string date, out DateTime result)
+    {
+        if (date == null) throw new ArgumentNullException("date");
+
+        if (date.Length < 22 || date.Length > 26 || date[14] != '.' || date[0] != '2' || date[1] != '0')
+            return DateTime.TryParse(date, out result);
+
+        try
+        {
+            var timezonePos = date.IndexOfAny(new[]{'+', '-'});
+            var isPlus = date[timezonePos] == '+';
+            var timeZone = date.Substring(timezonePos + 1);
+            date = date.Substring(0, timezonePos);
+
+            var date2 = DateTime.ParseExact(date, "yyyyMMddHHmmss.ffffff", CultureInfo.InvariantCulture);
+            result = date2;
+
+            var timeZoneMinutes = int.Parse(timeZone);
+            //get utc by remving timezone adjustment
+            result = isPlus
+                ? date2.AddMinutes(-timeZoneMinutes)
+                : date2.AddMinutes(timeZoneMinutes);
+                
+            return true;
+        }
+        catch (Exception ex)
+        {
+            _logger.Error($"Failed to convert {date}.", ex);
+        }
+
+        return DateTime.TryParse(date, out result);
+    }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/UpdateSimilaritiesFromNewReport.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/UpdateSimilaritiesFromNewReport.cs
new file mode 100644
index 00000000..b3cc91be
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/Handlers/UpdateSimilaritiesFromNewReport.cs
@@ -0,0 +1,100 @@
+using System;
+using System.Diagnostics;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Modules.Similarities;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using Coderr.Server.ReportAnalyzer.Similarities.Adapters.Runner;
+using Coderr.Server.ReportAnalyzer.Similarities.Handlers.Processing;
+using DotNetCqs;
+using Coderr.Server.Abstractions.Boot;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities.Handlers
+{
+    /// 
+    ///     Responsible of analyzing the reports Context Data to find similarities from all reports in an incident.
+    /// 
+    public class UpdateSimilaritiesFromNewReport : IMessageHandler
+    {
+        private readonly AdapterRepository _adapterRepository = new AdapterRepository();
+        private readonly ILog _logger = LogManager.GetLogger(typeof(UpdateSimilaritiesFromNewReport));
+        private readonly ISimilarityRepository _similarityRepository;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repository
+        /// similarityRepository
+        public UpdateSimilaritiesFromNewReport(ISimilarityRepository similarityRepository)
+        {
+            _similarityRepository = similarityRepository ?? throw new ArgumentNullException(nameof(similarityRepository));
+        }
+
+        /// 
+        ///     Process an event asynchronously.
+        /// 
+        /// event to process
+        /// 
+        ///     Task to wait on.
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            if (e.IsStored != true)
+            {
+                return;
+            }
+
+            _logger.Debug("Updating similarities " + e.Incident.Id);
+            var adapters = _adapterRepository.GetAdapters();
+            var sw2 = new Stopwatch();
+            sw2.Start();
+            long beginStep, afterFindStep, afterReposStep;
+
+            try
+            {
+                _logger.Debug("Finding for incident: " + e.Incident.Id);
+                beginStep = sw2.ElapsedMilliseconds;
+
+                var similaritiesReport = _similarityRepository.FindForIncident(e.Incident.Id);
+                var isNew = false;
+                if (similaritiesReport == null)
+                {
+                    similaritiesReport = new SimilaritiesReport(e.Incident.Id);
+                    isNew = true;
+                }
+
+                var analyzer = new SimilarityAnalyzer(similaritiesReport);
+                afterFindStep = sw2.ElapsedMilliseconds;
+                analyzer.AddReport(e.Report, adapters);
+
+                if (isNew)
+                {
+                    _logger.Debug("Creating new similarity report...");
+                    await _similarityRepository.CreateAsync(similaritiesReport);
+                }
+                else
+                {
+                    _logger.Debug("Updating existing similarity report...");
+                    await _similarityRepository.UpdateAsync(similaritiesReport);
+                }
+
+                afterReposStep = sw2.ElapsedMilliseconds;
+                _logger.Debug("similarities done ");
+            }
+            catch (Exception exception)
+            {
+                _logger.Error("failed to add report to incident " + e.Incident.Id, exception);
+
+                // Live changes since we get deadlocks?
+                // TODO: WHY do we get deadlocks, aren't we the only ones reading from the similarity tables?
+                return;
+            }
+            sw2.Stop();
+            if (sw2.ElapsedMilliseconds > 200)
+            {
+                _logger.InfoFormat("Slow similarity handling, times: {0}/{1}/{2}/{3}", beginStep, afterFindStep, afterReposStep,
+                    sw2.ElapsedMilliseconds);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Similarities/SimilarityAnalyzer.cs b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/SimilarityAnalyzer.cs
new file mode 100644
index 00000000..871ef36f
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Similarities/SimilarityAnalyzer.cs
@@ -0,0 +1,83 @@
+using System;
+using System.Collections.Generic;
+using Coderr.Server.Domain.Modules.Similarities;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Similarities.Adapters;
+
+namespace Coderr.Server.ReportAnalyzer.Similarities
+{
+    class SimilarityAnalyzer
+    {
+        private readonly SimilaritiesReport _similarity;
+
+        public SimilarityAnalyzer(SimilaritiesReport similarity)
+        {
+            _similarity = similarity;
+        }
+        /// 
+        ///     Add a new report.
+        /// 
+        /// report
+        /// adapters
+        public void AddReport(ReportDTO report, IReadOnlyList adapters)
+        {
+            if (report == null) throw new ArgumentNullException("report");
+            if (adapters == null) throw new ArgumentNullException("adapters");
+
+            //TODO: Varför skicka in adapters? Skapa via en Factory istället
+            if (report == null) throw new ArgumentNullException("report");
+
+            _similarity.IncreateReportCount();
+
+            foreach (var context in report.ContextCollections)
+            {
+                if (context.Name == null)
+                    throw new ArgumentException("ContextInfo.Name may not be null.");
+
+                
+                if (_similarity.IsIgnored(context.Name))
+                    continue;
+
+                foreach (var property in context.Properties)
+                {
+                    if (property.Value != null && property.Value.Length > 40)
+                        continue;
+
+                    if (property.Key.Equals("OEMStringArray"))
+                        continue;
+                    if (context.Name.Equals("ExceptionProperties") && property.Key == "Message")
+                        continue;
+                    if (context.Name.Equals("ExceptionProperties") && property.Key == "StackTrace")
+                        continue;
+                    if (context.Name.Equals("ExceptionProperties") && property.Key == "InnerException")
+                        continue;
+                    if (property.Key.Contains("LastModified"))
+                        continue;
+                    if (property.Key == "Id")
+                        continue;
+
+                    var adapterContext = new ValueAdapterContext(context.Name, property.Key, property.Value, report);
+                    object adaptedValue = property.Value;
+                    foreach (var adapter in adapters)
+                    {
+                        adaptedValue = adapter.Adapt(adapterContext, adaptedValue);
+                    }
+
+                    foreach (var field in adapterContext.CustomFields)
+                    {
+                        _similarity.AddSimilarity(field.ContextName, field.PropertyName, field.Value);
+                    }
+
+                    if (adapterContext.IgnoreProperty || "".Equals(adaptedValue))
+                        continue;
+
+                    if (adaptedValue == null)
+                        adaptedValue = adapterContext.Value ?? "null";
+                    _similarity.AddSimilarity(context.Name, adapterContext.PropertyName, adaptedValue);
+                    //similarity.IncreaseUsage(ReportCount);
+                }
+            }
+        }
+
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/AssemblyScanningTagIdentifierProvider.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/AssemblyScanningTagIdentifierProvider.cs
new file mode 100644
index 00000000..edbaf7b2
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/AssemblyScanningTagIdentifierProvider.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging
+{
+    /// 
+    ///     Used to provide tag identifiers (i.e. classes which can identify StackOverflow.com tags by using the report
+    ///     information)
+    /// 
+    [Obsolete]
+    public class AssemblyScanningTagIdentifierProvider : ITagIdentifierProvider
+    {
+        private static readonly List Identifiers = new List();
+
+        [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline",
+            Justification = "How on earth could I do that?")]
+        static AssemblyScanningTagIdentifierProvider()
+        {
+            foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
+            {
+                if (type.IsAbstract || type.IsInterface || !typeof(ITagIdentifier).IsAssignableFrom(type))
+                    continue;
+
+                Identifiers.Add((ITagIdentifier) Activator.CreateInstance(type));
+            }
+        }
+
+        /// 
+        ///     Get all identifies for the given context.
+        /// 
+        /// context
+        /// all identified identifiers.
+        /// context
+        [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
+        public IEnumerable GetIdentifiers(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            return Identifiers;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IdentifyTagsFromIncident.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IdentifyTagsFromIncident.cs
new file mode 100644
index 00000000..6efad316
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IdentifyTagsFromIncident.cs
@@ -0,0 +1,99 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Tagging.Events;
+using Coderr.Server.Domain.Modules.Tags;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Handlers
+{
+    /// 
+    ///     Scan through the error report to identify which libraries were used when the exception was thrown.
+    /// 
+    public class IdentifyTagsFromIncident : IMessageHandler
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(IdentifyTagsFromIncident));
+        private readonly ITagsRepository _repository;
+        private readonly ITagIdentifierProvider _tagIdentifierProvider;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repository
+        /// Used to be able to create tag identifiers in all modules
+        /// repository
+        public IdentifyTagsFromIncident(ITagsRepository repository, ITagIdentifierProvider tagIdentifierProvider)
+        {
+            _repository = repository ?? throw new ArgumentNullException(nameof(repository));
+            _tagIdentifierProvider = tagIdentifierProvider;
+        }
+
+        /// 
+        ///     Process an event asynchronously.
+        /// 
+        /// event to process
+        /// 
+        ///     Task to wait on.
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            _logger.Debug("Checking tags..");
+            var tags = await _repository.GetIncidentTagsAsync(e.Incident.Id);
+            var ctx = new TagIdentifierContext(e.Report, tags);
+            var identifiers = _tagIdentifierProvider.GetIdentifiers(ctx);
+            foreach (var identifier in identifiers)
+            {
+                identifier.Identify(ctx);
+            }
+
+            ExtractTagsFromCollections(e, ctx);
+
+            _logger.DebugFormat("Done, identified {0} new tags", string.Join(",", ctx.NewTags));
+
+            if (ctx.NewTags.Count == 0)
+                return;
+
+            await _repository.AddAsync(e.Incident.Id, ctx.NewTags.ToArray());
+
+            var newTagsAsStrings = ctx.NewTags.Select(x => x.Name).ToArray();
+            await context.SendAsync(
+                new TagAttachedToIncident(e.Incident.ApplicationId, e.Incident.Id, newTagsAsStrings));
+        }
+
+        private void ExtractTagsFromCollections(ReportAddedToIncident e, TagIdentifierContext ctx)
+        {
+            foreach (var collection in e.Report.ContextCollections)
+            {
+                // Comma separated tags
+                if (collection.Properties.TryGetValue("OneTrueTags", out var tagsStr)
+                    || collection.Properties.TryGetValue("ErrTags", out tagsStr))
+                {
+                    try
+                    {
+                        var tags = tagsStr.Split(',');
+                        foreach (var tag in tags)
+                        {
+                            _logger.Debug($"Adding tag '{tag}' to incident {e.Incident.Id}");
+                            ctx.AddTag(tag.Trim(), 1);
+                        }
+                    }
+                    catch (Exception ex)
+                    {
+                        _logger.Error(
+                            "Failed to parse tags from '" + collection.Name + "', invalid tag string: '" + tagsStr + "'.",
+                            ex);
+                    }
+                }
+
+                //Tag array
+                foreach (var property in collection.Properties)
+                {
+                    if (property.Key.StartsWith("ErrTags["))
+                        ctx.AddTag(property.Value.Trim(), 1);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IncidentReopenedHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IncidentReopenedHandler.cs
new file mode 100644
index 00000000..2bbf500f
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Handlers/IncidentReopenedHandler.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.Incidents.Events;
+using Coderr.Server.Domain.Modules.Tags;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Handlers
+{
+    /// 
+    ///     Adds a "incident-reopened" tag
+    /// 
+    public class IncidentReopenedHandler : IMessageHandler
+    {
+        private readonly ITagsRepository _repository;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repos
+        /// repository
+        public IncidentReopenedHandler(ITagsRepository repository)
+        {
+            if (repository == null) throw new ArgumentNullException("repository");
+            _repository = repository;
+        }
+
+        /// 
+        public async Task HandleAsync(IMessageContext context, IncidentReOpened e)
+        {
+            var tags = await _repository.GetIncidentTagsAsync(e.IncidentId);
+            if (tags.Any(x => x.Name == "incident-reopened"))
+                return;
+
+            await _repository.AddAsync(e.IncidentId, new[] {new Tag("incident-reopened", 1)});
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/ITagIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/ITagIdentifier.cs
similarity index 82%
rename from src/Server/OneTrueError.App/Modules/Tagging/ITagIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/ITagIdentifier.cs
index b2a0d8b7..0b5c48cb 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/ITagIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/ITagIdentifier.cs
@@ -1,15 +1,15 @@
-namespace OneTrueError.App.Modules.Tagging
-{
-    /// 
-    ///     Purpose of this interface is to provide a way to be able to search through the error report context to be able to
-    ///     identify stackoverflow tags.
-    /// 
-    public interface ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        void Identify(TagIdentifierContext context);
-    }
+namespace Coderr.Server.ReportAnalyzer.Tagging
+{
+    /// 
+    ///     Purpose of this interface is to provide a way to be able to search through the error report context to be able to
+    ///     identify StackOverflow.com tags.
+    /// 
+    public interface ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        void Identify(TagIdentifierContext context);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/ITagIdentifierProvider.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/ITagIdentifierProvider.cs
new file mode 100644
index 00000000..02ed2a4d
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/ITagIdentifierProvider.cs
@@ -0,0 +1,20 @@
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging
+{
+    /// 
+    ///     A tag identifier is run on every inbound report with the task to find a set of tags that tells what kind of
+    ///     incident this is.
+    /// 
+    public interface ITagIdentifierProvider
+    {
+        /// 
+        ///     Get all identifies for the given context.
+        /// 
+        /// information about the report and the incident
+        /// all identified identifiers.
+        /// context
+        IEnumerable GetIdentifiers(TagIdentifierContext context);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/AdoNetIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AdoNetIdentifier.cs
similarity index 83%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/AdoNetIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AdoNetIdentifier.cs
index dc6947b1..43183022 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/AdoNetIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AdoNetIdentifier.cs
@@ -1,22 +1,22 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Adds the "ADO.NET" tag if "System.Data" assembly have been loaded.
-    /// 
-    [Component]
-    public class AdoNetIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            context.AddIfFound("System.Data", "ado.net");
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Adds the "ADO.NET" tag if "System.Data" assembly have been loaded.
+    /// 
+    [ContainerService]
+    public class AdoNetIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            context.AddIfFound("System.Data", "ado.net");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs
similarity index 90%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs
index 2d83eb1d..1fad38ac 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/AspNetMvcAndWebApiIdentifier.cs
@@ -1,47 +1,51 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Identifies ASP.NET MVC and WebApi including their versions.
-    /// 
-    [Component]
-    public class AspNetMvcAndWebApiIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            var orderNumber = context.AddIfFound("System.Web.Mvc", "asp.net-mvc");
-            if (orderNumber != -1)
-            {
-                var propertyValue = context.GetPropertyValue("Assemblies", "System.Web.Mvc");
-                if (!string.IsNullOrEmpty(propertyValue))
-                {
-                    var version = propertyValue.Substring(0, 1);
-                    if (version != "0" && version != "1")
-                        context.AddTag("asp.net-mvc-" + version, orderNumber);
-                }
-
-                context.AddTag("asp.net", 99);
-            }
-
-            orderNumber = context.AddIfFound("System.Web.Http.WebHost", "asp.net-web-api");
-            if (orderNumber != -1)
-            {
-                var propertyValue = context.GetPropertyValue("Assemblies", "System.Web.Http.WebHost");
-                if (!string.IsNullOrEmpty(propertyValue))
-                {
-                    var version = propertyValue.Substring(0, 1);
-                    context.AddTag("asp.net-web-api-" + version, orderNumber);
-                }
-
-                context.AddTag("asp.net", 99);
-            }
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Identifies ASP.NET MVC and WebApi including their versions.
+    /// 
+    [ContainerService]
+    public class AspNetMvcAndWebApiIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+
+
+            context.AddIfFound("System.Web", "http");
+
+            var orderNumber = context.AddIfFound("System.Web.Mvc", "asp.net-mvc");
+            if (orderNumber != -1)
+            {
+                var propertyValue = context.GetPropertyValue("Assemblies", "System.Web.Mvc");
+                if (!string.IsNullOrEmpty(propertyValue))
+                {
+                    var version = propertyValue.Substring(0, 1);
+                    if (version != "0" && version != "1")
+                        context.AddTag("asp.net-mvc-" + version, orderNumber);
+                }
+
+                context.AddTag("asp.net", 99);
+            }
+
+            orderNumber = context.AddIfFound("System.Web.Http.WebHost", "asp.net-web-api");
+            if (orderNumber != -1)
+            {
+                var propertyValue = context.GetPropertyValue("Assemblies", "System.Web.Http.WebHost");
+                if (!string.IsNullOrEmpty(propertyValue))
+                {
+                    var version = propertyValue.Substring(0, 1);
+                    context.AddTag("asp.net-web-api-" + version, orderNumber);
+                }
+
+                context.AddTag("asp.net", 99);
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/CSharpIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/CSharpIdentifier.cs
similarity index 84%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/CSharpIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/CSharpIdentifier.cs
index e97d5592..34318eba 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/CSharpIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/CSharpIdentifier.cs
@@ -1,25 +1,25 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Checks if C# is loaded and which version of it.
-    /// 
-    [Component]
-    internal class CSharpIdentifier : ITagIdentifier
-    {
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            var property = context.GetPropertyValue("Assembly", "Microsoft.CSharp");
-            if (property == null)
-                return;
-
-            context.AddTag("c#", 99);
-            var pos = property.IndexOf(".");
-            if (pos != -1)
-                context.AddTag("c#-" + property.Substring(pos + 1, 3), 99);
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Checks if C# is loaded and which version of it.
+    /// 
+    [ContainerService]
+    internal class CSharpIdentifier : ITagIdentifier
+    {
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            var property = context.GetPropertyValue("Assembly", "Microsoft.CSharp");
+            if (property == null)
+                return;
+
+            context.AddTag("c#", 99);
+            var pos = property.IndexOf(".");
+            if (pos != -1)
+                context.AddTag("c#-" + property.Substring(pos + 1, 3), 99);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/ConsoleApplication.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/ConsoleApplication.cs
similarity index 85%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/ConsoleApplication.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/ConsoleApplication.cs
index 3a0d10b6..1b23d322 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/ConsoleApplication.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/ConsoleApplication.cs
@@ -1,26 +1,26 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Checks if the application was run as an console application.
-    /// 
-    [Component]
-    public class ConsoleApplication : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-
-            if (context.IsFound("System.Windows.Forms"))
-                return;
-
-            context.AddIfFound("Program.Main(String[] args)", "console-application");
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Checks if the application was run as an console application.
+    /// 
+    [ContainerService]
+    public class ConsoleApplication : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+
+            if (context.IsFound("System.Windows.Forms"))
+                return;
+
+            context.AddIfFound("Program.Main(String[] args)", "console-application");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/Datacontract.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/Datacontract.cs
similarity index 87%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/Datacontract.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/Datacontract.cs
index aa408a9f..7bf33bcf 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/Datacontract.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/Datacontract.cs
@@ -1,24 +1,24 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Checks if [DataContract] is loaded.
-    /// 
-    [Component]
-    public class DataContract : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            context.AddIfFound("System.Runtime.Serialization", "datacontractserializer");
-            context.AddIfFound("System.Runtime.Serialization.XmlObjectSerializer", "xml-serialization");
-            context.AddIfFound("System.Runtime.Serialization.XmlObjectSerializer.WriteObject", "xml-serialization");
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Checks if [DataContract] is loaded.
+    /// 
+    [ContainerService]
+    public class DataContract : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            context.AddIfFound("System.Runtime.Serialization", "datacontractserializer");
+            context.AddIfFound("System.Runtime.Serialization.XmlObjectSerializer", "xml-serialization");
+            context.AddIfFound("System.Runtime.Serialization.XmlObjectSerializer.WriteObject", "xml-serialization");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/EntityFrameworkIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/EntityFrameworkIdentifier.cs
similarity index 90%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/EntityFrameworkIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/EntityFrameworkIdentifier.cs
index f3a3ad9d..5e91365d 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/EntityFrameworkIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/EntityFrameworkIdentifier.cs
@@ -1,36 +1,36 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Looks for the EntityFramework.SqlServer assembly in the stack trace.
-    /// 
-    [Component]
-    public class EntityFrameworkIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-
-            var orderNumber = context.AddIfFound("EntityFramework", "entity-framework");
-            if (orderNumber != -1)
-            {
-                var propertyValue = context.GetPropertyValue("Assemblies", "entity-framework");
-                if (!string.IsNullOrEmpty(propertyValue))
-                {
-                    context.AddTag("entity-framework-" + propertyValue.Substring(0, 1), orderNumber);
-                }
-            }
-            var property2 = context.GetPropertyValue("Assemblies", "EntityFramework.SqlServer");
-            if (property2 != null)
-            {
-                context.AddTag("sqlserver", 99);
-            }
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Looks for the EntityFramework.SqlServer assembly in the stack trace.
+    /// 
+    [ContainerService]
+    public class EntityFrameworkIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+
+            var orderNumber = context.AddIfFound("EntityFramework", "entity-framework");
+            if (orderNumber != -1)
+            {
+                var propertyValue = context.GetPropertyValue("Assemblies", "entity-framework");
+                if (!string.IsNullOrEmpty(propertyValue))
+                {
+                    context.AddTag("entity-framework-" + propertyValue.Substring(0, 1), orderNumber);
+                }
+            }
+            var property2 = context.GetPropertyValue("Assemblies", "EntityFramework.SqlServer");
+            if (property2 != null)
+            {
+                context.AddTag("sqlserver", 99);
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/JsonNetIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/JsonNetIdentifier.cs
similarity index 82%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/JsonNetIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/JsonNetIdentifier.cs
index cd566317..fd4e661d 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/JsonNetIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/JsonNetIdentifier.cs
@@ -1,22 +1,22 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Identifies Newtonsoft.Json
-    /// 
-    [Component]
-    public class JsonNetIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            context.AddIfFound("Newtonsoft.Json", "json.net");
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Identifies Newtonsoft.Json
+    /// 
+    [ContainerService]
+    public class JsonNetIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            context.AddIfFound("Newtonsoft.Json", "json.net");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/LinqIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/LinqIdentifier.cs
similarity index 83%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/LinqIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/LinqIdentifier.cs
index a7e4bc4f..f4cba5e3 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/LinqIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/LinqIdentifier.cs
@@ -1,23 +1,23 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Identifies if the exception had LINQ in the stack trace.
-    /// 
-    [Component]
-    public class LinqIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-
-            context.AddIfFound("System.Linq.Enumerable", "linq");
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Identifies if the exception had LINQ in the stack trace.
+    /// 
+    [ContainerService]
+    public class LinqIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+
+            context.AddIfFound("System.Linq.Enumerable", "linq");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/MarkdownSharp.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/MarkdownSharp.cs
similarity index 88%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/MarkdownSharp.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/MarkdownSharp.cs
index fb57079b..6f72cc54 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/MarkdownSharp.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/MarkdownSharp.cs
@@ -1,20 +1,20 @@
-using System;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Identifies the MarkdownSharp library
-    /// 
-    public class MarkdownSharpIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            context.AddIfFound("MarkdownSharp", "MarkdownSharp");
-        }
-    }
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Identifies the MarkdownSharp library
+    /// 
+    public class MarkdownSharpIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            context.AddIfFound("MarkdownSharp", "MarkdownSharp");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/NHibernate.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/NHibernate.cs
similarity index 89%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/NHibernate.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/NHibernate.cs
index a459312e..f49932cc 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/NHibernate.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/NHibernate.cs
@@ -1,29 +1,29 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Identifies nhibernate, fluent-nhibernate and other nhibernate related assemblies.
-    /// 
-    [Component]
-    public class NHibernate : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            context.AddIfFound("FluentNHibernate", "fluent-nhibernate");
-            context.AddIfFound("FluentNHibernate.Mapping", "fluent-nhibernate-mapping");
-            context.AddIfFound("NHibernate.", "nhibernate");
-            context.AddIfFound("NHibernate.Criterion", "nhibernate-criteria");
-            context.AddIfFound("NHibernate.Linq", "linq-to-nhibernate");
-            context.AddIfFound("NHibernate.Mapping.", "nhibernate-mapping");
-
-            //linq-to-nhibernate
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Identifies nhibernate, fluent-nhibernate and other nhibernate related assemblies.
+    /// 
+    [ContainerService]
+    public class NHibernate : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            context.AddIfFound("FluentNHibernate", "fluent-nhibernate");
+            context.AddIfFound("FluentNHibernate.Mapping", "fluent-nhibernate-mapping");
+            context.AddIfFound("NHibernate.", "nhibernate");
+            context.AddIfFound("NHibernate.Criterion", "nhibernate-criteria");
+            context.AddIfFound("NHibernate.Linq", "linq-to-nhibernate");
+            context.AddIfFound("NHibernate.Mapping.", "nhibernate-mapping");
+
+            //linq-to-nhibernate
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/RazorIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/RazorIdentifier.cs
similarity index 85%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/RazorIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/RazorIdentifier.cs
index 1b9ba149..8904092c 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/RazorIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/RazorIdentifier.cs
@@ -1,25 +1,25 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Identify Razor View Engine.
-    /// 
-    [Component]
-    internal class RazorIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-
-            var propertyValue = context.GetPropertyValue("ExceptionProperties", "Message");
-            if (propertyValue != null && propertyValue.Contains(".cshtml"))
-                context.AddTag("razor", 0);
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Identify Razor View Engine.
+    /// 
+    [ContainerService]
+    internal class RazorIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+
+            var propertyValue = context.GetPropertyValue("ExceptionProperties", "Message");
+            if (propertyValue != null && propertyValue.Contains(".cshtml"))
+                context.AddTag("razor", 0);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/SqlServerIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/SqlServerIdentifier.cs
similarity index 84%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/SqlServerIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/SqlServerIdentifier.cs
index 399cce86..927846c2 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/SqlServerIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/SqlServerIdentifier.cs
@@ -1,24 +1,24 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Add the "sql-server" tag-
-    /// 
-    [Component]
-    public class SqlServerIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            context.AddIfFound("System.Data.SqlClient", "sql-server");
-
-            //TODO: Can we identify the version in some way?
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Add the "sql-server" tag-
+    /// 
+    [ContainerService]
+    public class SqlServerIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            context.AddIfFound("System.Data.SqlClient", "sql-server");
+
+            //TODO: Can we identify the version in some way?
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/WcfIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/WcfIdentifier.cs
similarity index 75%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/WcfIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/WcfIdentifier.cs
index 2b8327a7..b58f17ea 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/WcfIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/WcfIdentifier.cs
@@ -1,19 +1,19 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Checks if WCF is loaded.
-    /// 
-    [Component]
-    internal class WcfIdentifier : ITagIdentifier
-    {
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-
-            context.AddIfFound("System.ServiceModel", "wcf");
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Checks if WCF is loaded.
+    /// 
+    [ContainerService]
+    internal class WcfIdentifier : ITagIdentifier
+    {
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+
+            context.AddIfFound("System.ServiceModel", "wcf");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/WinFormsIdentifier.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/WinFormsIdentifier.cs
similarity index 82%
rename from src/Server/OneTrueError.App/Modules/Tagging/Identifiers/WinFormsIdentifier.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/WinFormsIdentifier.cs
index 58ed36cd..028a2e82 100644
--- a/src/Server/OneTrueError.App/Modules/Tagging/Identifiers/WinFormsIdentifier.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/Identifiers/WinFormsIdentifier.cs
@@ -1,22 +1,22 @@
-using System;
-using Griffin.Container;
-
-namespace OneTrueError.App.Modules.Tagging.Identifiers
-{
-    /// 
-    ///     Checks if WinForms is loaded.
-    /// 
-    [Component]
-    public class WinFormsIdentifier : ITagIdentifier
-    {
-        /// 
-        ///     Check if the wanted tag is supported.
-        /// 
-        /// Error context providing information to search through
-        public void Identify(TagIdentifierContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            context.AddIfFound("System.Windows.Forms", "winforms");
-        }
-    }
+using System;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging.Identifiers
+{
+    /// 
+    ///     Checks if WinForms is loaded.
+    /// 
+    [ContainerService]
+    public class WinFormsIdentifier : ITagIdentifier
+    {
+        /// 
+        ///     Check if the wanted tag is supported.
+        /// 
+        /// Error context providing information to search through
+        public void Identify(TagIdentifierContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            context.AddIfFound("System.Windows.Forms", "winforms");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/IocIdentifierProvider.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/IocIdentifierProvider.cs
new file mode 100644
index 00000000..cb5a86a3
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/IocIdentifierProvider.cs
@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Coderr.Server.Abstractions.Boot;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging
+{
+    /// 
+    ///     Uses the Container to find all tag identifiers.
+    /// 
+    [ContainerService]
+    public class IocIdentifierProvider : ITagIdentifierProvider
+    {
+        private readonly ITagIdentifier[] _identifiers;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// 
+        public IocIdentifierProvider(IEnumerable identifiers)
+        {
+            _identifiers = identifiers.ToArray();
+        }
+
+        /// 
+        public IEnumerable GetIdentifiers(TagIdentifierContext context)
+        {
+            return _identifiers;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Tagging/TagIdentifierContext.cs b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/TagIdentifierContext.cs
new file mode 100644
index 00000000..9d7ba0f3
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Tagging/TagIdentifierContext.cs
@@ -0,0 +1,124 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Coderr.Server.Domain.Modules.Tags;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+
+namespace Coderr.Server.ReportAnalyzer.Tagging
+{
+    /// 
+    ///     Context used when trying to identify StackOverflow.com tags
+    /// 
+    public class TagIdentifierContext
+    {
+        private readonly IReadOnlyList _existingTags;
+        private readonly List _newTags = new List();
+        private readonly ReportDTO _reportToAnalyze;
+        private readonly string[] _stacktrace;
+
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// rta
+        /// 
+        /// reportToAnalyze;tags
+        public TagIdentifierContext(ReportDTO reportToAnalyze, IReadOnlyList tags)
+        {
+            _reportToAnalyze = reportToAnalyze ?? throw new ArgumentNullException(nameof(reportToAnalyze));
+            _existingTags = tags ?? throw new ArgumentNullException(nameof(tags));
+            var ex = reportToAnalyze.Exception;
+            _stacktrace = ex?
+                              .StackTrace?
+                              .Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries)
+                          ?? new string[0];
+        }
+
+        /// 
+        ///     Application that the report is for.
+        /// 
+        public int ApplicationId => _reportToAnalyze.ApplicationId;
+
+        /// 
+        ///     Exception to find tags for.
+        /// 
+        /// 
+        ///     Tags identifying relevant tags which can be used to find information about why the exception happened. Like
+        ///     "EntityFramework", "ASP.NET-MVC" etc.
+        /// 
+        /// 
+        ///     These tags are used directly to search for possible solutions.
+        /// 
+        public IReadOnlyList NewTags => _newTags;
+
+        /// 
+        ///     Add tag if the specified text is found in the stack trace
+        /// 
+        /// text to find
+        /// tag to add
+        /// index in stacktrace if found; otherwise -1
+        /// libraryToFind;tagToAdd
+        public int AddIfFound(string libraryToFind, string tagToAdd)
+        {
+            if (libraryToFind == null) throw new ArgumentNullException(nameof(libraryToFind));
+            if (tagToAdd == null) throw new ArgumentNullException(nameof(tagToAdd));
+
+            for (var i = 0; i < _stacktrace.Length; i++)
+            {
+                if (_stacktrace[i].IndexOf(libraryToFind, StringComparison.OrdinalIgnoreCase) == -1)
+                    continue;
+
+                AddTag(tagToAdd, i);
+                return i;
+            }
+
+            return -1;
+        }
+
+        /// 
+        ///     Add an incident tag
+        /// 
+        /// tag name
+        /// used to customize in which order the tags appear on the web page. 1 = first
+        public void AddTag(string tag, int orderNumber)
+        {
+            if (_newTags.Any(x => x.Name.Equals(tag, StringComparison.OrdinalIgnoreCase)))
+                return;
+            if (_existingTags.Any(x => x.Name.Equals(tag, StringComparison.OrdinalIgnoreCase)))
+                return;
+
+            _newTags.Add(new Tag(tag, orderNumber));
+        }
+
+        /// 
+        ///     Get a property value from a context collection
+        /// 
+        /// context collection
+        /// property in the collection
+        /// collectionName; propertyName
+        /// value if found; otherwise null.
+        public string GetPropertyValue(string collectionName, string propertyName)
+        {
+            if (collectionName == null) throw new ArgumentNullException(nameof(collectionName));
+            if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
+
+            var assemblies = Enumerable.FirstOrDefault(_reportToAnalyze.ContextCollections, x => x.Name == collectionName);
+            if (assemblies == null)
+                return null;
+
+            return assemblies.Properties.TryGetValue(propertyName, out string version) ? version : null;
+        }
+
+        /// 
+        ///     Find a text in the stack trace
+        /// 
+        /// text to find
+        /// libraryToFind
+        /// true if found; otherwise false.
+        public bool IsFound(string libraryToFind)
+        {
+            if (libraryToFind == null) throw new ArgumentNullException(nameof(libraryToFind));
+            return _stacktrace.Any(t => t.IndexOf(libraryToFind, StringComparison.OrdinalIgnoreCase) != -1);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ActionConfigurationData.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ActionConfigurationData.cs
new file mode 100644
index 00000000..2c7d8193
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ActionConfigurationData.cs
@@ -0,0 +1,28 @@
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Defines information for a specific action in a trigger.
+    /// 
+    /// 
+    ///     
+    ///         "Send email", for instance, might have email address as .
+    ///     
+    /// 
+    public class ActionConfigurationData
+    {
+        /// 
+        ///     Action to take
+        /// 
+        public string ActionName { get; set; }
+
+        /// 
+        ///     Context data for the action.
+        /// 
+        public string Data { get; set; }
+
+        /// 
+        ///     Primary key
+        /// 
+        public int Id { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ActionExecutionContext.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ActionExecutionContext.cs
similarity index 78%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/ActionExecutionContext.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/ActionExecutionContext.cs
index 0cec4508..02504723 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ActionExecutionContext.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ActionExecutionContext.cs
@@ -1,26 +1,26 @@
-using OneTrueError.Api.Core.Incidents;
-using OneTrueError.Api.Core.Reports;
-
-namespace OneTrueError.App.Modules.Triggers.Domain
-{
-    /// 
-    ///     Execution information for a trigger action.
-    /// 
-    public class ActionExecutionContext
-    {
-        /// 
-        ///     Config for this action step
-        /// 
-        public ActionConfigurationData Config { get; set; }
-
-        /// 
-        ///     Report that the trigger is running on.
-        /// 
-        public ReportDTO ErrorReport { get; set; }
-
-        /// 
-        ///     Incident that the trigger is runnning for.
-        /// 
-        public IncidentSummaryDTO Incident { get; set; }
-    }
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Execution information for a trigger action.
+    /// 
+    public class ActionExecutionContext
+    {
+        /// 
+        ///     Config for this action step
+        /// 
+        public ActionConfigurationData Config { get; set; }
+
+        /// 
+        ///     Report that the trigger is running on.
+        /// 
+        public ReportDTO ErrorReport { get; set; }
+
+        /// 
+        ///     Incident that the trigger is runnning for.
+        /// 
+        public IncidentSummaryDTO Incident { get; set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/HttpPostAction.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/HttpPostAction.cs
similarity index 93%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/HttpPostAction.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/HttpPostAction.cs
index 77dadf0d..caf50d0c 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/HttpPostAction.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/HttpPostAction.cs
@@ -1,40 +1,40 @@
-using System;
-using System.Net;
-using System.Text;
-using System.Threading.Tasks;
-using log4net;
-using Newtonsoft.Json;
-
-namespace OneTrueError.App.Modules.Triggers.Domain.Actions
-{
-    /// 
-    ///     Do a HTTP post in a trigger
-    /// 
-    [TriggerActionName("Http")]
-    public class HttpPostAction : ITriggerAction
-    {
-        private readonly ILog _log = LogManager.GetLogger(typeof(HttpPostAction));
-
-        /// 
-        ///     POSTs data using JSON. Json object is { Report = ErrorReport, Incident = incident }
-        /// 
-        /// trigger action context
-        public async Task ExecuteAsync(ActionExecutionContext context)
-        {
-            try
-            {
-                var request = WebRequest.CreateHttp(context.Config.Data);
-                request.ContentType = "application/json";
-                var stream = await request.GetRequestStreamAsync();
-                var json = JsonConvert.SerializeObject(new {Report = context.ErrorReport, context.Incident});
-                var buffer = Encoding.UTF8.GetBytes(json);
-                stream.Write(buffer, 0, buffer.Length);
-                await request.GetResponseAsync();
-            }
-            catch (Exception exception)
-            {
-                _log.Error("Failed to contact " + context.Config.Data, exception);
-            }
-        }
-    }
+using System;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using log4net;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Actions
+{
+    /// 
+    ///     Do a HTTP post in a trigger
+    /// 
+    [TriggerActionName("Http")]
+    public class HttpPostAction : ITriggerAction
+    {
+        private readonly ILog _log = LogManager.GetLogger(typeof(HttpPostAction));
+
+        /// 
+        ///     POSTs data using JSON. Json object is { Report = ErrorReport, Incident = incident }
+        /// 
+        /// trigger action context
+        public async Task ExecuteAsync(ActionExecutionContext context)
+        {
+            try
+            {
+                var request = WebRequest.CreateHttp(context.Config.Data);
+                request.ContentType = "application/json";
+                var stream = await request.GetRequestStreamAsync();
+                var json = JsonConvert.SerializeObject(new {Report = context.ErrorReport, context.Incident});
+                var buffer = Encoding.UTF8.GetBytes(json);
+                stream.Write(buffer, 0, buffer.Length);
+                await request.GetResponseAsync();
+            }
+            catch (Exception exception)
+            {
+                _log.Error("Failed to contact " + context.Config.Data, exception);
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/ITriggerAction.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/ITriggerAction.cs
similarity index 85%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/ITriggerAction.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/ITriggerAction.cs
index ca0f97de..d1ee1e12 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/ITriggerAction.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/ITriggerAction.cs
@@ -1,16 +1,16 @@
-using System.Threading.Tasks;
-
-namespace OneTrueError.App.Modules.Triggers.Domain.Actions
-{
-    /// 
-    ///     Represents trigger actions, i.e. the work that should be done once all rules have accepted the report.
-    /// 
-    public interface ITriggerAction
-    {
-        /// 
-        ///     Execute the action.
-        /// 
-        /// action context
-        Task ExecuteAsync(ActionExecutionContext context);
-    }
+using System.Threading.Tasks;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Actions
+{
+    /// 
+    ///     Represents trigger actions, i.e. the work that should be done once all rules have accepted the report.
+    /// 
+    public interface ITriggerAction
+    {
+        /// 
+        ///     Execute the action.
+        /// 
+        /// action context
+        Task ExecuteAsync(ActionExecutionContext context);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/NotifyActionType.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/NotifyActionType.cs
similarity index 81%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/NotifyActionType.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/NotifyActionType.cs
index 1151c4f0..0abf544a 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/NotifyActionType.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/NotifyActionType.cs
@@ -1,18 +1,18 @@
-namespace OneTrueError.App.Modules.Triggers.Domain.Actions
-{
-    /// 
-    ///     When to notify users
-    /// 
-    public enum NotifyActionType
-    {
-        /// 
-        ///     notify if filter validates to false
-        /// 
-        NotifyOnFailure,
-
-        /// 
-        ///     notify on filter success
-        /// 
-        NotifyOnSuccess
-    }
+namespace Coderr.Server.ReportAnalyzer.Triggers.Actions
+{
+    /// 
+    ///     When to notify users
+    /// 
+    public enum NotifyActionType
+    {
+        /// 
+        ///     notify if filter validates to false
+        /// 
+        NotifyOnFailure,
+
+        /// 
+        ///     notify on filter success
+        /// 
+        NotifyOnSuccess
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/NotifyUsersActionSettings.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/NotifyUsersActionSettings.cs
similarity index 88%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/NotifyUsersActionSettings.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/NotifyUsersActionSettings.cs
index 6f1b8029..352d1f5b 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/NotifyUsersActionSettings.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/NotifyUsersActionSettings.cs
@@ -1,22 +1,22 @@
-using System.Diagnostics.CodeAnalysis;
-
-namespace OneTrueError.App.Modules.Triggers.Domain.Actions
-{
-    /// 
-    ///     Settings
-    /// 
-    public class NotifyUsersActionSettings
-    {
-        /// 
-        ///     When to notify the users
-        /// 
-        public NotifyActionType NotificationType { get; set; }
-
-        /// 
-        ///     Can either be emails (contains '@') or account ids
-        /// 
-        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays",
-            Justification = "I like my arrays.")]
-        public string[] Targets { get; set; }
-    }
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Actions
+{
+    /// 
+    ///     Settings
+    /// 
+    public class NotifyUsersActionSettings
+    {
+        /// 
+        ///     When to notify the users
+        /// 
+        public NotifyActionType NotificationType { get; set; }
+
+        /// 
+        ///     Can either be emails (contains '@') or account ids
+        /// 
+        [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays",
+            Justification = "I like my arrays.")]
+        public string[] Targets { get; set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/SendEmail.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/SendEmail.cs
new file mode 100644
index 00000000..afd34b09
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/SendEmail.cs
@@ -0,0 +1,16 @@
+using System.Threading.Tasks;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Actions
+{
+    [TriggerActionName("Email")]
+    internal class SendEmailTask : ITriggerAction
+    {
+        public Task ExecuteAsync(ActionExecutionContext context)
+        {
+            //TODO: Create a generic emailer with symbols as the default
+            // notification system is currently much better than this notification thingy.
+            //i.e. it do not add any value currently.
+            return Task.FromResult(null);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/SendSmsAction.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/SendSmsAction.cs
similarity index 84%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/SendSmsAction.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/SendSmsAction.cs
index 3b97c41c..50081487 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/SendSmsAction.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/SendSmsAction.cs
@@ -1,69 +1,75 @@
-using System;
-using System.Net;
-using System.Text;
-using System.Threading.Tasks;
-using log4net;
-using Newtonsoft.Json;
-using OneTrueError.App.Configuration;
-using OneTrueError.Infrastructure.Configuration;
-
-namespace OneTrueError.App.Modules.Triggers.Domain.Actions
-{
-    /// 
-    ///     Send SMS (through Gauffin Interactive)
-    /// 
-    [TriggerActionName("Sms")]
-    public class SendSmsAction : ITriggerAction
-    {
-        private readonly ILog _log = LogManager.GetLogger(typeof(SendSmsAction));
-
-        /// 
-        ///     Execute action
-        /// 
-        /// 
-        public async Task ExecuteAsync(ActionExecutionContext context)
-        {
-            if (context == null) throw new ArgumentNullException("context");
-            try
-            {
-                var config = ConfigurationStore.Instance.Load();
-                var baseUrl = config.BaseUrl;
-                //TODO: Add title
-                var msg = "";
-                if (context.Incident.ReportCount == 1)
-                {
-                    msg = string.Format("{2}\r\nurl: {0}/incident/{1}", baseUrl, context.Incident.Id,
-                        context.Incident.Name);
-                }
-                else
-                {
-                    msg = string.Format("{0}\r\nreport count: {4}\r\nurl: {1}/incident/{2}/report/{3}\r\n",
-                        context.Incident.Name, baseUrl, context.Incident.Id, context.ErrorReport.ReportId,
-                        context.Incident.ReportCount);
-                }
-
-                //TODO: Move to our service.
-                var iso = Encoding.GetEncoding("ISO-8859-1");
-                var utfBytes = Encoding.UTF8.GetBytes(msg);
-                var isoBytes = Encoding.Convert(Encoding.UTF8, iso, utfBytes);
-                msg = iso.GetString(isoBytes);
-
-                var request =
-                    WebRequest.CreateHttp("https://web.smscom.se/sendsms.aspx?acc=ip1-755&pass=z35llww4&msg=" +
-                                          Uri.EscapeDataString(msg) + "&to=" + context.Config.Data +
-                                          "&from=OneTrueError&prio=2");
-                request.ContentType = "application/json";
-                request.Method = "POST";
-                var stream = await request.GetRequestStreamAsync();
-                var json = JsonConvert.SerializeObject(new {Report = context.ErrorReport, context.Incident});
-                var buffer = Encoding.UTF8.GetBytes(json);
-                stream.Write(buffer, 0, buffer.Length);
-                await request.GetResponseAsync();
-            }
-            catch (Exception exception)
-            {
-                _log.Error("Failed to contact " + context.Config.Data, exception);
-            }
-        }
-    }
+using System;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Infrastructure.Configuration;
+using log4net;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Actions
+{
+    /// 
+    ///     Send SMS (through Gauffin Interactive)
+    /// 
+    [TriggerActionName("Sms")]
+    public class SendSmsAction : ITriggerAction
+    {
+        private readonly ILog _log = LogManager.GetLogger(typeof(SendSmsAction));
+        private ConfigurationStore _configStore;
+
+        public SendSmsAction(ConfigurationStore configStore)
+        {
+            _configStore = configStore;
+        }
+
+        /// 
+        ///     Execute action
+        /// 
+        /// 
+        public async Task ExecuteAsync(ActionExecutionContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            try
+            {
+                var config = _configStore.Load();
+                var baseUrl = config.BaseUrl;
+                //TODO: Add title
+                var msg = "";
+                if (context.Incident.ReportCount == 1)
+                {
+                    msg = string.Format("{2}\r\nurl: {0}/incident/{1}", baseUrl, context.Incident.Id,
+                        context.Incident.Name);
+                }
+                else
+                {
+                    msg = string.Format("{0}\r\nreport count: {4}\r\nurl: {1}/incident/{2}/report/{3}\r\n",
+                        context.Incident.Name, baseUrl, context.Incident.Id, context.ErrorReport.ReportId,
+                        context.Incident.ReportCount);
+                }
+
+                //TODO: Move to our service.
+                var iso = Encoding.GetEncoding("ISO-8859-1");
+                var utfBytes = Encoding.UTF8.GetBytes(msg);
+                var isoBytes = Encoding.Convert(Encoding.UTF8, iso, utfBytes);
+                msg = iso.GetString(isoBytes);
+
+                var request =
+                    WebRequest.CreateHttp("https://web.smscom.se/sendsms.aspx?acc=ip1-755&pass=z35llww4&msg=" +
+                                          Uri.EscapeDataString(msg) + "&to=" + context.Config.Data +
+                                          "&from=Coderr&prio=2");
+                request.ContentType = "application/json";
+                request.Method = "POST";
+                var stream = await request.GetRequestStreamAsync();
+                var json = JsonConvert.SerializeObject(new {Report = context.ErrorReport, context.Incident});
+                var buffer = Encoding.UTF8.GetBytes(json);
+                stream.Write(buffer, 0, buffer.Length);
+                await request.GetResponseAsync();
+            }
+            catch (Exception exception)
+            {
+                _log.Error("Failed to contact " + context.Config.Data, exception);
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/Tools/GSMEncoding.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/Tools/GSMEncoding.cs
similarity index 97%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/Tools/GSMEncoding.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/Tools/GSMEncoding.cs
index 089a5d74..c386ccb8 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/Tools/GSMEncoding.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Actions/Tools/GSMEncoding.cs
@@ -1,532 +1,532 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-/*
- * Copyright (c) 2010 Mediaburst Ltd 
- *
- * Permission to use, copy, modify, and/or distribute this software for any
- * purpose with or without fee is hereby granted, provided that the above
- * copyright notice and this permission notice appear in all copies.
- *
- * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- */
-
-namespace OneTrueError.App.Modules.Triggers.Domain.Actions.Tools
-{
-    /// 
-    ///     Text encoding class for the GSM 03.38 alphabet.
-    ///     Converts between GSM and the internal .NET Unicode character representation
-    /// 
-    public class GsmEncoding : Encoding
-    {
-        private SortedDictionary _byteToChar;
-        private SortedDictionary _charToByte;
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        public GsmEncoding()
-        {
-            PopulateDictionaries();
-        }
-
-        /// 
-        ///     When overridden in a derived class, calculates the number of bytes produced by encoding a set of characters from
-        ///     the specified character array.
-        /// 
-        /// 
-        ///     The number of bytes produced by encoding the specified characters.
-        /// 
-        /// The character array containing the set of characters to encode. 
-        /// The index of the first character to encode. 
-        /// The number of characters to encode. 
-        ///  is null. 
-        /// 
-        ///      or  is less
-        ///     than zero.-or-  and  do not denote a valid range in
-        ///     .
-        /// 
-        /// 
-        ///     A fallback occurred (see Character Encoding in the .NET
-        ///     Framework for complete explanation)-and- is set to
-        ///     .
-        /// 
-        /// 1
-        public override int GetByteCount(char[] chars, int index, int count)
-        {
-            var byteCount = 0;
-
-            if (chars == null)
-            {
-                throw new ArgumentNullException("chars");
-            }
-            if (index < 0 || index > chars.Length)
-            {
-                throw new ArgumentOutOfRangeException("index");
-            }
-            if (count < 0 || count > chars.Length - index)
-            {
-                throw new ArgumentOutOfRangeException("count");
-            }
-
-            for (var i = index; i < count; i++)
-            {
-                if (_charToByte.ContainsKey(chars[i]))
-                {
-                    byteCount += _charToByte[chars[i]].Length;
-                }
-            }
-
-            return byteCount;
-        }
-
-        /// 
-        ///     When overridden in a derived class, encodes a set of characters from the specified character array into the
-        ///     specified byte array.
-        /// 
-        /// 
-        ///     The actual number of bytes written into .
-        /// 
-        /// The character array containing the set of characters to encode. 
-        /// The index of the first character to encode. 
-        /// The number of characters to encode. 
-        /// The byte array to contain the resulting sequence of bytes. 
-        /// The index at which to start writing the resulting sequence of bytes. 
-        /// 
-        ///      is null.-or-  is
-        ///     null.
-        /// 
-        /// 
-        ///      or 
-        ///     or  is less than zero.-or-  and
-        ///      do not denote a valid range in .-or-
-        ///      is not a valid index in .
-        /// 
-        /// 
-        ///      does not have enough capacity from
-        ///      to the end of the array to accommodate the resulting bytes.
-        /// 
-        /// 
-        ///     A fallback occurred (see Character Encoding in the .NET
-        ///     Framework for complete explanation)-and- is set to
-        ///     .
-        /// 
-        /// 1
-        public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
-        {
-            var byteCount = 0;
-
-            // Validate the parameters.
-            if (chars == null)
-            {
-                throw new ArgumentNullException("chars");
-            }
-            if (bytes == null)
-            {
-                throw new ArgumentNullException("bytes");
-            }
-            if (charIndex < 0 || charIndex > chars.Length)
-            {
-                throw new ArgumentOutOfRangeException("charIndex");
-            }
-            if (charCount < 0 || charCount > chars.Length - charIndex)
-            {
-                throw new ArgumentOutOfRangeException("charCount");
-            }
-            if (byteIndex < 0 || byteIndex > bytes.Length)
-            {
-                throw new ArgumentOutOfRangeException("byteIndex");
-            }
-            if (byteIndex + GetByteCount(chars, charIndex, charCount) > bytes.Length)
-            {
-                throw new ArgumentException("bytes array too small", "bytes");
-            }
-            for (var i = charIndex; i < charIndex + charCount; i++)
-            {
-                byte[] charByte;
-                if (_charToByte.TryGetValue(chars[i], out charByte))
-                {
-                    charByte.CopyTo(bytes, byteIndex + byteCount);
-                    byteCount += charByte.Length;
-                }
-            }
-            return byteCount;
-        }
-
-        /// 
-        ///     When overridden in a derived class, calculates the number of characters produced by decoding a sequence of bytes
-        ///     from the specified byte array.
-        /// 
-        /// 
-        ///     The number of characters produced by decoding the specified sequence of bytes.
-        /// 
-        /// The byte array containing the sequence of bytes to decode. 
-        /// The index of the first byte to decode. 
-        /// The number of bytes to decode. 
-        ///  is null. 
-        /// 
-        ///      or  is less
-        ///     than zero.-or-  and  do not denote a valid range in
-        ///     .
-        /// 
-        /// 
-        ///     A fallback occurred (see Character Encoding in the .NET
-        ///     Framework for complete explanation)-and- is set to
-        ///     .
-        /// 
-        /// 1
-        public override int GetCharCount(byte[] bytes, int index, int count)
-        {
-            var charCount = 0;
-
-            if (bytes == null)
-            {
-                throw new ArgumentNullException("bytes");
-            }
-            if (index < 0 || index > bytes.Length)
-            {
-                throw new ArgumentOutOfRangeException("index");
-            }
-            if (count < 0 || count > bytes.Length - index)
-            {
-                throw new ArgumentOutOfRangeException("count");
-            }
-
-            var i = index;
-            while (i < index + count)
-            {
-                if (bytes[i] <= 0x7F)
-                {
-                    if (bytes[i] == 0x1B)
-                    {
-                        i++;
-                        if (i < bytes.Length && bytes[i] <= 0x7F)
-                        {
-                            charCount++; // GSM Spec says replace 1B 1B with space
-                        }
-                    }
-                    else
-                    {
-                        charCount++;
-                    }
-                }
-                i++;
-            }
-
-            return charCount;
-        }
-
-        /// 
-        ///     When overridden in a derived class, decodes a sequence of bytes from the specified byte array into the specified
-        ///     character array.
-        /// 
-        /// 
-        ///     The actual number of characters written into .
-        /// 
-        /// The byte array containing the sequence of bytes to decode. 
-        /// The index of the first byte to decode. 
-        /// The number of bytes to decode. 
-        /// The character array to contain the resulting set of characters. 
-        /// The index at which to start writing the resulting set of characters. 
-        /// 
-        ///      is null.-or-  is
-        ///     null.
-        /// 
-        /// 
-        ///      or 
-        ///     or  is less than zero.-or-  and
-        ///      do not denote a valid range in .-or-
-        ///      is not a valid index in .
-        /// 
-        /// 
-        ///      does not have enough capacity from
-        ///      to the end of the array to accommodate the resulting characters.
-        /// 
-        /// 
-        ///     A fallback occurred (see Character Encoding in the .NET
-        ///     Framework for complete explanation)-and- is set to
-        ///     .
-        /// 
-        /// 1
-        public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
-        {
-            var charCount = 0;
-
-            // Validate the parameters.
-            if (bytes == null)
-            {
-                throw new ArgumentNullException("bytes");
-            }
-            if (chars == null)
-            {
-                throw new ArgumentNullException("chars");
-            }
-            if (byteIndex < 0 || byteIndex > bytes.Length)
-            {
-                throw new ArgumentOutOfRangeException("byteIndex");
-            }
-            if (byteCount < 0 || byteCount > bytes.Length - byteIndex)
-            {
-                throw new ArgumentOutOfRangeException("byteCount");
-            }
-            if (charIndex < 0 || charIndex > chars.Length)
-            {
-                throw new ArgumentOutOfRangeException("charIndex");
-            }
-            if (charIndex + GetCharCount(bytes, byteIndex, byteCount) > chars.Length)
-            {
-                throw new ArgumentException("chars array too small", "chars");
-            }
-
-
-            var i = byteIndex;
-            while (i < byteIndex + byteCount)
-            {
-                if (bytes[i] <= 0x7F)
-                {
-                    if (bytes[i] == 0x1B)
-                    {
-                        i++;
-                        if (i < bytes.Length && bytes[i] <= 0x7F)
-                        {
-                            char nextChar;
-                            var extendedChar = 0x1B*255 + (uint) bytes[i];
-                            if (_byteToChar.TryGetValue(extendedChar, out nextChar))
-                            {
-                                chars[charCount] = nextChar;
-                                charCount++;
-                            }
-                            // GSM Spec says to try for normal character if escaped one doesn't exist
-                            else if (_byteToChar.TryGetValue(bytes[i], out nextChar))
-                            {
-                                chars[charCount] = nextChar;
-                                charCount++;
-                            }
-                        }
-                    }
-                    else
-                    {
-                        chars[charCount] = _byteToChar[bytes[i]];
-                        charCount++;
-                    }
-                }
-                i++;
-            }
-
-            return charCount;
-        }
-
-        /// 
-        ///     When overridden in a derived class, calculates the maximum number of bytes produced by encoding the specified
-        ///     number of characters.
-        /// 
-        /// 
-        ///     The maximum number of bytes produced by encoding the specified number of characters.
-        /// 
-        /// The number of characters to encode. 
-        ///  is less than zero. 
-        /// 
-        ///     A fallback occurred (see Character Encoding in the .NET
-        ///     Framework for complete explanation)-and- is set to
-        ///     .
-        /// 
-        /// 1
-        public override int GetMaxByteCount(int charCount)
-        {
-            if (charCount < 0)
-                throw new ArgumentOutOfRangeException("charCount");
-
-            return charCount*2;
-        }
-
-        /// 
-        ///     When overridden in a derived class, calculates the maximum number of characters produced by decoding the specified
-        ///     number of bytes.
-        /// 
-        /// 
-        ///     The maximum number of characters produced by decoding the specified number of bytes.
-        /// 
-        /// The number of bytes to decode. 
-        ///  is less than zero. 
-        /// 
-        ///     A fallback occurred (see Character Encoding in the .NET
-        ///     Framework for complete explanation)-and- is set to
-        ///     .
-        /// 
-        /// 1
-        public override int GetMaxCharCount(int byteCount)
-        {
-            if (byteCount < 0)
-                throw new ArgumentOutOfRangeException("byteCount");
-
-            return byteCount;
-        }
-
-
-        private void PopulateDictionaries()
-        {
-            // Unicode char to GSM bytes
-            _charToByte = new SortedDictionary();
-            // GSM bytes to Unicode char
-            _byteToChar = new SortedDictionary();
-
-            _charToByte.Add('\u0040', new byte[] {0x00});
-            _charToByte.Add('\u00A3', new byte[] {0x01});
-            _charToByte.Add('\u0024', new byte[] {0x02});
-            _charToByte.Add('\u00A5', new byte[] {0x03});
-            _charToByte.Add('\u00E8', new byte[] {0x04});
-            _charToByte.Add('\u00E9', new byte[] {0x05});
-            _charToByte.Add('\u00F9', new byte[] {0x06});
-            _charToByte.Add('\u00EC', new byte[] {0x07});
-            _charToByte.Add('\u00F2', new byte[] {0x08});
-            _charToByte.Add('\u00C7', new byte[] {0x09});
-            _charToByte.Add('\u000A', new byte[] {0x0A});
-            _charToByte.Add('\u00D8', new byte[] {0x0B});
-            _charToByte.Add('\u00F8', new byte[] {0x0C});
-            _charToByte.Add('\u000D', new byte[] {0x0D});
-            _charToByte.Add('\u00C5', new byte[] {0x0E});
-            _charToByte.Add('\u00E5', new byte[] {0x0F});
-            _charToByte.Add('\u0394', new byte[] {0x10});
-            _charToByte.Add('\u005F', new byte[] {0x11});
-            _charToByte.Add('\u03A6', new byte[] {0x12});
-            _charToByte.Add('\u0393', new byte[] {0x13});
-            _charToByte.Add('\u039B', new byte[] {0x14});
-            _charToByte.Add('\u03A9', new byte[] {0x15});
-            _charToByte.Add('\u03A0', new byte[] {0x16});
-            _charToByte.Add('\u03A8', new byte[] {0x17});
-            _charToByte.Add('\u03A3', new byte[] {0x18});
-            _charToByte.Add('\u0398', new byte[] {0x19});
-            _charToByte.Add('\u039E', new byte[] {0x1A});
-            //_charToByte.Add('\u001B', new byte[] { 0x1B }); // Should we convert Unicode escape to GSM?
-            _charToByte.Add('\u00C6', new byte[] {0x1C});
-            _charToByte.Add('\u00E6', new byte[] {0x1D});
-            _charToByte.Add('\u00DF', new byte[] {0x1E});
-            _charToByte.Add('\u00C9', new byte[] {0x1F});
-            _charToByte.Add('\u0020', new byte[] {0x20});
-            _charToByte.Add('\u0021', new byte[] {0x21});
-            _charToByte.Add('\u0022', new byte[] {0x22});
-            _charToByte.Add('\u0023', new byte[] {0x23});
-            _charToByte.Add('\u00A4', new byte[] {0x24});
-            _charToByte.Add('\u0025', new byte[] {0x25});
-            _charToByte.Add('\u0026', new byte[] {0x26});
-            _charToByte.Add('\u0027', new byte[] {0x27});
-            _charToByte.Add('\u0028', new byte[] {0x28});
-            _charToByte.Add('\u0029', new byte[] {0x29});
-            _charToByte.Add('\u002A', new byte[] {0x2A});
-            _charToByte.Add('\u002B', new byte[] {0x2B});
-            _charToByte.Add('\u002C', new byte[] {0x2C});
-            _charToByte.Add('\u002D', new byte[] {0x2D});
-            _charToByte.Add('\u002E', new byte[] {0x2E});
-            _charToByte.Add('\u002F', new byte[] {0x2F});
-            _charToByte.Add('\u0030', new byte[] {0x30});
-            _charToByte.Add('\u0031', new byte[] {0x31});
-            _charToByte.Add('\u0032', new byte[] {0x32});
-            _charToByte.Add('\u0033', new byte[] {0x33});
-            _charToByte.Add('\u0034', new byte[] {0x34});
-            _charToByte.Add('\u0035', new byte[] {0x35});
-            _charToByte.Add('\u0036', new byte[] {0x36});
-            _charToByte.Add('\u0037', new byte[] {0x37});
-            _charToByte.Add('\u0038', new byte[] {0x38});
-            _charToByte.Add('\u0039', new byte[] {0x39});
-            _charToByte.Add('\u003A', new byte[] {0x3A});
-            _charToByte.Add('\u003B', new byte[] {0x3B});
-            _charToByte.Add('\u003C', new byte[] {0x3C});
-            _charToByte.Add('\u003D', new byte[] {0x3D});
-            _charToByte.Add('\u003E', new byte[] {0x3E});
-            _charToByte.Add('\u003F', new byte[] {0x3F});
-            _charToByte.Add('\u00A1', new byte[] {0x40});
-            _charToByte.Add('\u0041', new byte[] {0x41});
-            _charToByte.Add('\u0042', new byte[] {0x42});
-            _charToByte.Add('\u0043', new byte[] {0x43});
-            _charToByte.Add('\u0044', new byte[] {0x44});
-            _charToByte.Add('\u0045', new byte[] {0x45});
-            _charToByte.Add('\u0046', new byte[] {0x46});
-            _charToByte.Add('\u0047', new byte[] {0x47});
-            _charToByte.Add('\u0048', new byte[] {0x48});
-            _charToByte.Add('\u0049', new byte[] {0x49});
-            _charToByte.Add('\u004A', new byte[] {0x4A});
-            _charToByte.Add('\u004B', new byte[] {0x4B});
-            _charToByte.Add('\u004C', new byte[] {0x4C});
-            _charToByte.Add('\u004D', new byte[] {0x4D});
-            _charToByte.Add('\u004E', new byte[] {0x4E});
-            _charToByte.Add('\u004F', new byte[] {0x4F});
-            _charToByte.Add('\u0050', new byte[] {0x50});
-            _charToByte.Add('\u0051', new byte[] {0x51});
-            _charToByte.Add('\u0052', new byte[] {0x52});
-            _charToByte.Add('\u0053', new byte[] {0x53});
-            _charToByte.Add('\u0054', new byte[] {0x54});
-            _charToByte.Add('\u0055', new byte[] {0x55});
-            _charToByte.Add('\u0056', new byte[] {0x56});
-            _charToByte.Add('\u0057', new byte[] {0x57});
-            _charToByte.Add('\u0058', new byte[] {0x58});
-            _charToByte.Add('\u0059', new byte[] {0x59});
-            _charToByte.Add('\u005A', new byte[] {0x5A});
-            _charToByte.Add('\u00C4', new byte[] {0x5B});
-            _charToByte.Add('\u00D6', new byte[] {0x5C});
-            _charToByte.Add('\u00D1', new byte[] {0x5D});
-            _charToByte.Add('\u00DC', new byte[] {0x5E});
-            _charToByte.Add('\u00A7', new byte[] {0x5F});
-            _charToByte.Add('\u00BF', new byte[] {0x60});
-            _charToByte.Add('\u0061', new byte[] {0x61});
-            _charToByte.Add('\u0062', new byte[] {0x62});
-            _charToByte.Add('\u0063', new byte[] {0x63});
-            _charToByte.Add('\u0064', new byte[] {0x64});
-            _charToByte.Add('\u0065', new byte[] {0x65});
-            _charToByte.Add('\u0066', new byte[] {0x66});
-            _charToByte.Add('\u0067', new byte[] {0x67});
-            _charToByte.Add('\u0068', new byte[] {0x68});
-            _charToByte.Add('\u0069', new byte[] {0x69});
-            _charToByte.Add('\u006A', new byte[] {0x6A});
-            _charToByte.Add('\u006B', new byte[] {0x6B});
-            _charToByte.Add('\u006C', new byte[] {0x6C});
-            _charToByte.Add('\u006D', new byte[] {0x6D});
-            _charToByte.Add('\u006E', new byte[] {0x6E});
-            _charToByte.Add('\u006F', new byte[] {0x6F});
-            _charToByte.Add('\u0070', new byte[] {0x70});
-            _charToByte.Add('\u0071', new byte[] {0x71});
-            _charToByte.Add('\u0072', new byte[] {0x72});
-            _charToByte.Add('\u0073', new byte[] {0x73});
-            _charToByte.Add('\u0074', new byte[] {0x74});
-            _charToByte.Add('\u0075', new byte[] {0x75});
-            _charToByte.Add('\u0076', new byte[] {0x76});
-            _charToByte.Add('\u0077', new byte[] {0x77});
-            _charToByte.Add('\u0078', new byte[] {0x78});
-            _charToByte.Add('\u0079', new byte[] {0x79});
-            _charToByte.Add('\u007A', new byte[] {0x7A});
-            _charToByte.Add('\u00E4', new byte[] {0x7B});
-            _charToByte.Add('\u00F6', new byte[] {0x7C});
-            _charToByte.Add('\u00F1', new byte[] {0x7D});
-            _charToByte.Add('\u00FC', new byte[] {0x7E});
-            _charToByte.Add('\u00E0', new byte[] {0x7F});
-            // Extended GSM
-            _charToByte.Add('\u20AC', new byte[] {0x1B, 0x65});
-            _charToByte.Add('\u000C', new byte[] {0x1B, 0x0A});
-            _charToByte.Add('\u005B', new byte[] {0x1B, 0x3C});
-            _charToByte.Add('\u005C', new byte[] {0x1B, 0x2F});
-            _charToByte.Add('\u005D', new byte[] {0x1B, 0x3E});
-            _charToByte.Add('\u005E', new byte[] {0x1B, 0x14});
-            _charToByte.Add('\u007B', new byte[] {0x1B, 0x28});
-            _charToByte.Add('\u007C', new byte[] {0x1B, 0x40});
-            _charToByte.Add('\u007D', new byte[] {0x1B, 0x29});
-            _charToByte.Add('\u007E', new byte[] {0x1B, 0x3D});
-
-            foreach (var charToByte in _charToByte)
-            {
-                uint charByteVal = 0;
-                if (charToByte.Value.Length == 1)
-                    charByteVal = charToByte.Value[0];
-                else if (charToByte.Value.Length == 2)
-                    charByteVal = (uint) charToByte.Value[0]*255 + charToByte.Value[1];
-                _byteToChar.Add(charByteVal, charToByte.Key);
-            }
-            _byteToChar.Add(0x1B1B, '\u0020'); // GSM char set says to map 1B1B to a space
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+/*
+ * Copyright (c) 2010 Mediaburst Ltd 
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Actions.Tools
+{
+    /// 
+    ///     Text encoding class for the GSM 03.38 alphabet.
+    ///     Converts between GSM and the internal .NET Unicode character representation
+    /// 
+    public class GsmEncoding : Encoding
+    {
+        private SortedDictionary _byteToChar;
+        private SortedDictionary _charToByte;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        public GsmEncoding()
+        {
+            PopulateDictionaries();
+        }
+
+        /// 
+        ///     When overridden in a derived class, calculates the number of bytes produced by encoding a set of characters from
+        ///     the specified character array.
+        /// 
+        /// 
+        ///     The number of bytes produced by encoding the specified characters.
+        /// 
+        /// The character array containing the set of characters to encode. 
+        /// The index of the first character to encode. 
+        /// The number of characters to encode. 
+        ///  is null. 
+        /// 
+        ///      or  is less
+        ///     than zero.-or-  and  do not denote a valid range in
+        ///     .
+        /// 
+        /// 
+        ///     A fallback occurred (see Character Encoding in the .NET
+        ///     Framework for complete explanation)-and- is set to
+        ///     .
+        /// 
+        /// 1
+        public override int GetByteCount(char[] chars, int index, int count)
+        {
+            var byteCount = 0;
+
+            if (chars == null)
+            {
+                throw new ArgumentNullException("chars");
+            }
+            if (index < 0 || index > chars.Length)
+            {
+                throw new ArgumentOutOfRangeException("index");
+            }
+            if (count < 0 || count > chars.Length - index)
+            {
+                throw new ArgumentOutOfRangeException("count");
+            }
+
+            for (var i = index; i < count; i++)
+            {
+                if (_charToByte.ContainsKey(chars[i]))
+                {
+                    byteCount += _charToByte[chars[i]].Length;
+                }
+            }
+
+            return byteCount;
+        }
+
+        /// 
+        ///     When overridden in a derived class, encodes a set of characters from the specified character array into the
+        ///     specified byte array.
+        /// 
+        /// 
+        ///     The actual number of bytes written into .
+        /// 
+        /// The character array containing the set of characters to encode. 
+        /// The index of the first character to encode. 
+        /// The number of characters to encode. 
+        /// The byte array to contain the resulting sequence of bytes. 
+        /// The index at which to start writing the resulting sequence of bytes. 
+        /// 
+        ///      is null.-or-  is
+        ///     null.
+        /// 
+        /// 
+        ///      or 
+        ///     or  is less than zero.-or-  and
+        ///      do not denote a valid range in .-or-
+        ///      is not a valid index in .
+        /// 
+        /// 
+        ///      does not have enough capacity from
+        ///      to the end of the array to accommodate the resulting bytes.
+        /// 
+        /// 
+        ///     A fallback occurred (see Character Encoding in the .NET
+        ///     Framework for complete explanation)-and- is set to
+        ///     .
+        /// 
+        /// 1
+        public override int GetBytes(char[] chars, int charIndex, int charCount, byte[] bytes, int byteIndex)
+        {
+            var byteCount = 0;
+
+            // Validate the parameters.
+            if (chars == null)
+            {
+                throw new ArgumentNullException("chars");
+            }
+            if (bytes == null)
+            {
+                throw new ArgumentNullException("bytes");
+            }
+            if (charIndex < 0 || charIndex > chars.Length)
+            {
+                throw new ArgumentOutOfRangeException("charIndex");
+            }
+            if (charCount < 0 || charCount > chars.Length - charIndex)
+            {
+                throw new ArgumentOutOfRangeException("charCount");
+            }
+            if (byteIndex < 0 || byteIndex > bytes.Length)
+            {
+                throw new ArgumentOutOfRangeException("byteIndex");
+            }
+            if (byteIndex + GetByteCount(chars, charIndex, charCount) > bytes.Length)
+            {
+                throw new ArgumentException("bytes array too small", "bytes");
+            }
+            for (var i = charIndex; i < charIndex + charCount; i++)
+            {
+                byte[] charByte;
+                if (_charToByte.TryGetValue(chars[i], out charByte))
+                {
+                    charByte.CopyTo(bytes, byteIndex + byteCount);
+                    byteCount += charByte.Length;
+                }
+            }
+            return byteCount;
+        }
+
+        /// 
+        ///     When overridden in a derived class, calculates the number of characters produced by decoding a sequence of bytes
+        ///     from the specified byte array.
+        /// 
+        /// 
+        ///     The number of characters produced by decoding the specified sequence of bytes.
+        /// 
+        /// The byte array containing the sequence of bytes to decode. 
+        /// The index of the first byte to decode. 
+        /// The number of bytes to decode. 
+        ///  is null. 
+        /// 
+        ///      or  is less
+        ///     than zero.-or-  and  do not denote a valid range in
+        ///     .
+        /// 
+        /// 
+        ///     A fallback occurred (see Character Encoding in the .NET
+        ///     Framework for complete explanation)-and- is set to
+        ///     .
+        /// 
+        /// 1
+        public override int GetCharCount(byte[] bytes, int index, int count)
+        {
+            var charCount = 0;
+
+            if (bytes == null)
+            {
+                throw new ArgumentNullException("bytes");
+            }
+            if (index < 0 || index > bytes.Length)
+            {
+                throw new ArgumentOutOfRangeException("index");
+            }
+            if (count < 0 || count > bytes.Length - index)
+            {
+                throw new ArgumentOutOfRangeException("count");
+            }
+
+            var i = index;
+            while (i < index + count)
+            {
+                if (bytes[i] <= 0x7F)
+                {
+                    if (bytes[i] == 0x1B)
+                    {
+                        i++;
+                        if (i < bytes.Length && bytes[i] <= 0x7F)
+                        {
+                            charCount++; // GSM Spec says replace 1B 1B with space
+                        }
+                    }
+                    else
+                    {
+                        charCount++;
+                    }
+                }
+                i++;
+            }
+
+            return charCount;
+        }
+
+        /// 
+        ///     When overridden in a derived class, decodes a sequence of bytes from the specified byte array into the specified
+        ///     character array.
+        /// 
+        /// 
+        ///     The actual number of characters written into .
+        /// 
+        /// The byte array containing the sequence of bytes to decode. 
+        /// The index of the first byte to decode. 
+        /// The number of bytes to decode. 
+        /// The character array to contain the resulting set of characters. 
+        /// The index at which to start writing the resulting set of characters. 
+        /// 
+        ///      is null.-or-  is
+        ///     null.
+        /// 
+        /// 
+        ///      or 
+        ///     or  is less than zero.-or-  and
+        ///      do not denote a valid range in .-or-
+        ///      is not a valid index in .
+        /// 
+        /// 
+        ///      does not have enough capacity from
+        ///      to the end of the array to accommodate the resulting characters.
+        /// 
+        /// 
+        ///     A fallback occurred (see Character Encoding in the .NET
+        ///     Framework for complete explanation)-and- is set to
+        ///     .
+        /// 
+        /// 1
+        public override int GetChars(byte[] bytes, int byteIndex, int byteCount, char[] chars, int charIndex)
+        {
+            var charCount = 0;
+
+            // Validate the parameters.
+            if (bytes == null)
+            {
+                throw new ArgumentNullException("bytes");
+            }
+            if (chars == null)
+            {
+                throw new ArgumentNullException("chars");
+            }
+            if (byteIndex < 0 || byteIndex > bytes.Length)
+            {
+                throw new ArgumentOutOfRangeException("byteIndex");
+            }
+            if (byteCount < 0 || byteCount > bytes.Length - byteIndex)
+            {
+                throw new ArgumentOutOfRangeException("byteCount");
+            }
+            if (charIndex < 0 || charIndex > chars.Length)
+            {
+                throw new ArgumentOutOfRangeException("charIndex");
+            }
+            if (charIndex + GetCharCount(bytes, byteIndex, byteCount) > chars.Length)
+            {
+                throw new ArgumentException("chars array too small", "chars");
+            }
+
+
+            var i = byteIndex;
+            while (i < byteIndex + byteCount)
+            {
+                if (bytes[i] <= 0x7F)
+                {
+                    if (bytes[i] == 0x1B)
+                    {
+                        i++;
+                        if (i < bytes.Length && bytes[i] <= 0x7F)
+                        {
+                            char nextChar;
+                            var extendedChar = 0x1B*255 + (uint) bytes[i];
+                            if (_byteToChar.TryGetValue(extendedChar, out nextChar))
+                            {
+                                chars[charCount] = nextChar;
+                                charCount++;
+                            }
+                            // GSM Spec says to try for normal character if escaped one doesn't exist
+                            else if (_byteToChar.TryGetValue(bytes[i], out nextChar))
+                            {
+                                chars[charCount] = nextChar;
+                                charCount++;
+                            }
+                        }
+                    }
+                    else
+                    {
+                        chars[charCount] = _byteToChar[bytes[i]];
+                        charCount++;
+                    }
+                }
+                i++;
+            }
+
+            return charCount;
+        }
+
+        /// 
+        ///     When overridden in a derived class, calculates the maximum number of bytes produced by encoding the specified
+        ///     number of characters.
+        /// 
+        /// 
+        ///     The maximum number of bytes produced by encoding the specified number of characters.
+        /// 
+        /// The number of characters to encode. 
+        ///  is less than zero. 
+        /// 
+        ///     A fallback occurred (see Character Encoding in the .NET
+        ///     Framework for complete explanation)-and- is set to
+        ///     .
+        /// 
+        /// 1
+        public override int GetMaxByteCount(int charCount)
+        {
+            if (charCount < 0)
+                throw new ArgumentOutOfRangeException("charCount");
+
+            return charCount*2;
+        }
+
+        /// 
+        ///     When overridden in a derived class, calculates the maximum number of characters produced by decoding the specified
+        ///     number of bytes.
+        /// 
+        /// 
+        ///     The maximum number of characters produced by decoding the specified number of bytes.
+        /// 
+        /// The number of bytes to decode. 
+        ///  is less than zero. 
+        /// 
+        ///     A fallback occurred (see Character Encoding in the .NET
+        ///     Framework for complete explanation)-and- is set to
+        ///     .
+        /// 
+        /// 1
+        public override int GetMaxCharCount(int byteCount)
+        {
+            if (byteCount < 0)
+                throw new ArgumentOutOfRangeException("byteCount");
+
+            return byteCount;
+        }
+
+
+        private void PopulateDictionaries()
+        {
+            // Unicode char to GSM bytes
+            _charToByte = new SortedDictionary();
+            // GSM bytes to Unicode char
+            _byteToChar = new SortedDictionary();
+
+            _charToByte.Add('\u0040', new byte[] {0x00});
+            _charToByte.Add('\u00A3', new byte[] {0x01});
+            _charToByte.Add('\u0024', new byte[] {0x02});
+            _charToByte.Add('\u00A5', new byte[] {0x03});
+            _charToByte.Add('\u00E8', new byte[] {0x04});
+            _charToByte.Add('\u00E9', new byte[] {0x05});
+            _charToByte.Add('\u00F9', new byte[] {0x06});
+            _charToByte.Add('\u00EC', new byte[] {0x07});
+            _charToByte.Add('\u00F2', new byte[] {0x08});
+            _charToByte.Add('\u00C7', new byte[] {0x09});
+            _charToByte.Add('\u000A', new byte[] {0x0A});
+            _charToByte.Add('\u00D8', new byte[] {0x0B});
+            _charToByte.Add('\u00F8', new byte[] {0x0C});
+            _charToByte.Add('\u000D', new byte[] {0x0D});
+            _charToByte.Add('\u00C5', new byte[] {0x0E});
+            _charToByte.Add('\u00E5', new byte[] {0x0F});
+            _charToByte.Add('\u0394', new byte[] {0x10});
+            _charToByte.Add('\u005F', new byte[] {0x11});
+            _charToByte.Add('\u03A6', new byte[] {0x12});
+            _charToByte.Add('\u0393', new byte[] {0x13});
+            _charToByte.Add('\u039B', new byte[] {0x14});
+            _charToByte.Add('\u03A9', new byte[] {0x15});
+            _charToByte.Add('\u03A0', new byte[] {0x16});
+            _charToByte.Add('\u03A8', new byte[] {0x17});
+            _charToByte.Add('\u03A3', new byte[] {0x18});
+            _charToByte.Add('\u0398', new byte[] {0x19});
+            _charToByte.Add('\u039E', new byte[] {0x1A});
+            //_charToByte.Add('\u001B', new byte[] { 0x1B }); // Should we convert Unicode escape to GSM?
+            _charToByte.Add('\u00C6', new byte[] {0x1C});
+            _charToByte.Add('\u00E6', new byte[] {0x1D});
+            _charToByte.Add('\u00DF', new byte[] {0x1E});
+            _charToByte.Add('\u00C9', new byte[] {0x1F});
+            _charToByte.Add('\u0020', new byte[] {0x20});
+            _charToByte.Add('\u0021', new byte[] {0x21});
+            _charToByte.Add('\u0022', new byte[] {0x22});
+            _charToByte.Add('\u0023', new byte[] {0x23});
+            _charToByte.Add('\u00A4', new byte[] {0x24});
+            _charToByte.Add('\u0025', new byte[] {0x25});
+            _charToByte.Add('\u0026', new byte[] {0x26});
+            _charToByte.Add('\u0027', new byte[] {0x27});
+            _charToByte.Add('\u0028', new byte[] {0x28});
+            _charToByte.Add('\u0029', new byte[] {0x29});
+            _charToByte.Add('\u002A', new byte[] {0x2A});
+            _charToByte.Add('\u002B', new byte[] {0x2B});
+            _charToByte.Add('\u002C', new byte[] {0x2C});
+            _charToByte.Add('\u002D', new byte[] {0x2D});
+            _charToByte.Add('\u002E', new byte[] {0x2E});
+            _charToByte.Add('\u002F', new byte[] {0x2F});
+            _charToByte.Add('\u0030', new byte[] {0x30});
+            _charToByte.Add('\u0031', new byte[] {0x31});
+            _charToByte.Add('\u0032', new byte[] {0x32});
+            _charToByte.Add('\u0033', new byte[] {0x33});
+            _charToByte.Add('\u0034', new byte[] {0x34});
+            _charToByte.Add('\u0035', new byte[] {0x35});
+            _charToByte.Add('\u0036', new byte[] {0x36});
+            _charToByte.Add('\u0037', new byte[] {0x37});
+            _charToByte.Add('\u0038', new byte[] {0x38});
+            _charToByte.Add('\u0039', new byte[] {0x39});
+            _charToByte.Add('\u003A', new byte[] {0x3A});
+            _charToByte.Add('\u003B', new byte[] {0x3B});
+            _charToByte.Add('\u003C', new byte[] {0x3C});
+            _charToByte.Add('\u003D', new byte[] {0x3D});
+            _charToByte.Add('\u003E', new byte[] {0x3E});
+            _charToByte.Add('\u003F', new byte[] {0x3F});
+            _charToByte.Add('\u00A1', new byte[] {0x40});
+            _charToByte.Add('\u0041', new byte[] {0x41});
+            _charToByte.Add('\u0042', new byte[] {0x42});
+            _charToByte.Add('\u0043', new byte[] {0x43});
+            _charToByte.Add('\u0044', new byte[] {0x44});
+            _charToByte.Add('\u0045', new byte[] {0x45});
+            _charToByte.Add('\u0046', new byte[] {0x46});
+            _charToByte.Add('\u0047', new byte[] {0x47});
+            _charToByte.Add('\u0048', new byte[] {0x48});
+            _charToByte.Add('\u0049', new byte[] {0x49});
+            _charToByte.Add('\u004A', new byte[] {0x4A});
+            _charToByte.Add('\u004B', new byte[] {0x4B});
+            _charToByte.Add('\u004C', new byte[] {0x4C});
+            _charToByte.Add('\u004D', new byte[] {0x4D});
+            _charToByte.Add('\u004E', new byte[] {0x4E});
+            _charToByte.Add('\u004F', new byte[] {0x4F});
+            _charToByte.Add('\u0050', new byte[] {0x50});
+            _charToByte.Add('\u0051', new byte[] {0x51});
+            _charToByte.Add('\u0052', new byte[] {0x52});
+            _charToByte.Add('\u0053', new byte[] {0x53});
+            _charToByte.Add('\u0054', new byte[] {0x54});
+            _charToByte.Add('\u0055', new byte[] {0x55});
+            _charToByte.Add('\u0056', new byte[] {0x56});
+            _charToByte.Add('\u0057', new byte[] {0x57});
+            _charToByte.Add('\u0058', new byte[] {0x58});
+            _charToByte.Add('\u0059', new byte[] {0x59});
+            _charToByte.Add('\u005A', new byte[] {0x5A});
+            _charToByte.Add('\u00C4', new byte[] {0x5B});
+            _charToByte.Add('\u00D6', new byte[] {0x5C});
+            _charToByte.Add('\u00D1', new byte[] {0x5D});
+            _charToByte.Add('\u00DC', new byte[] {0x5E});
+            _charToByte.Add('\u00A7', new byte[] {0x5F});
+            _charToByte.Add('\u00BF', new byte[] {0x60});
+            _charToByte.Add('\u0061', new byte[] {0x61});
+            _charToByte.Add('\u0062', new byte[] {0x62});
+            _charToByte.Add('\u0063', new byte[] {0x63});
+            _charToByte.Add('\u0064', new byte[] {0x64});
+            _charToByte.Add('\u0065', new byte[] {0x65});
+            _charToByte.Add('\u0066', new byte[] {0x66});
+            _charToByte.Add('\u0067', new byte[] {0x67});
+            _charToByte.Add('\u0068', new byte[] {0x68});
+            _charToByte.Add('\u0069', new byte[] {0x69});
+            _charToByte.Add('\u006A', new byte[] {0x6A});
+            _charToByte.Add('\u006B', new byte[] {0x6B});
+            _charToByte.Add('\u006C', new byte[] {0x6C});
+            _charToByte.Add('\u006D', new byte[] {0x6D});
+            _charToByte.Add('\u006E', new byte[] {0x6E});
+            _charToByte.Add('\u006F', new byte[] {0x6F});
+            _charToByte.Add('\u0070', new byte[] {0x70});
+            _charToByte.Add('\u0071', new byte[] {0x71});
+            _charToByte.Add('\u0072', new byte[] {0x72});
+            _charToByte.Add('\u0073', new byte[] {0x73});
+            _charToByte.Add('\u0074', new byte[] {0x74});
+            _charToByte.Add('\u0075', new byte[] {0x75});
+            _charToByte.Add('\u0076', new byte[] {0x76});
+            _charToByte.Add('\u0077', new byte[] {0x77});
+            _charToByte.Add('\u0078', new byte[] {0x78});
+            _charToByte.Add('\u0079', new byte[] {0x79});
+            _charToByte.Add('\u007A', new byte[] {0x7A});
+            _charToByte.Add('\u00E4', new byte[] {0x7B});
+            _charToByte.Add('\u00F6', new byte[] {0x7C});
+            _charToByte.Add('\u00F1', new byte[] {0x7D});
+            _charToByte.Add('\u00FC', new byte[] {0x7E});
+            _charToByte.Add('\u00E0', new byte[] {0x7F});
+            // Extended GSM
+            _charToByte.Add('\u20AC', new byte[] {0x1B, 0x65});
+            _charToByte.Add('\u000C', new byte[] {0x1B, 0x0A});
+            _charToByte.Add('\u005B', new byte[] {0x1B, 0x3C});
+            _charToByte.Add('\u005C', new byte[] {0x1B, 0x2F});
+            _charToByte.Add('\u005D', new byte[] {0x1B, 0x3E});
+            _charToByte.Add('\u005E', new byte[] {0x1B, 0x14});
+            _charToByte.Add('\u007B', new byte[] {0x1B, 0x28});
+            _charToByte.Add('\u007C', new byte[] {0x1B, 0x40});
+            _charToByte.Add('\u007D', new byte[] {0x1B, 0x29});
+            _charToByte.Add('\u007E', new byte[] {0x1B, 0x3D});
+
+            foreach (var charToByte in _charToByte)
+            {
+                uint charByteVal = 0;
+                if (charToByte.Value.Length == 1)
+                    charByteVal = charToByte.Value[0];
+                else if (charToByte.Value.Length == 2)
+                    charByteVal = (uint) charToByte.Value[0]*255 + charToByte.Value[1];
+                _byteToChar.Add(charByteVal, charToByte.Key);
+            }
+            _byteToChar.Add(0x1B1B, '\u0020'); // GSM char set says to map 1B1B to a space
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/CollectionMetadata.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/CollectionMetadata.cs
new file mode 100644
index 00000000..d6a35fbb
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/CollectionMetadata.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Contains information about a collection in a specific application
+    /// 
+    /// 
+    ///     
+    ///         This metadata will be used to enable autocomplete when designing triggers for the application.
+    ///     
+    /// 
+    public class CollectionMetadata
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Application identity (i.e. primary key)
+        /// Name as specified in the client library
+        public CollectionMetadata(int applicationId, string name)
+        {
+            if (name == null) throw new ArgumentNullException("name");
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
+
+            Name = name;
+            ApplicationId = applicationId;
+            Properties = new List();
+            IsUpdated = false;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected CollectionMetadata()
+        {
+            IsUpdated = false;
+        }
+
+        /// 
+        ///     Application that the incident belongs to
+        /// 
+        public int ApplicationId { get; private set; }
+
+        /// 
+        ///     Collection identity (unique between all collections, while the name is just unique for the referenced incident).
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     Incident has been updated.
+        /// 
+        [JsonIgnore]
+        public bool IsUpdated { get; private set; }
+
+        /// 
+        ///     Name as specified in the client library
+        /// 
+        public string Name { get; private set; }
+
+        /// 
+        ///     Properties collected by the client library.
+        /// 
+        public ICollection Properties { get; private set; }
+
+        /// 
+        ///     Add or update a property.
+        /// 
+        /// Property name
+        public void AddOrUpdateProperty(string name)
+        {
+            if (Properties.Contains(name))
+                return;
+
+            IsUpdated = true;
+            Properties.Add(name);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterCondition.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterCondition.cs
new file mode 100644
index 00000000..124cb391
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterCondition.cs
@@ -0,0 +1,35 @@
+using System.ComponentModel;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Specifies how the filter value should be compared with the actual property data.
+    /// 
+    public enum FilterCondition
+    {
+        /// 
+        ///     Should start with the given value
+        /// 
+        [Description("Starts with")] StartsWith,
+
+        /// 
+        ///     Should end with the given value
+        /// 
+        [Description("Ends with")] EndsWith,
+
+        /// 
+        ///     Should contain the given value
+        /// 
+        [Description("Contain")] Contains,
+
+        /// 
+        ///     Should not contain the given value
+        /// 
+        [Description("Do not contain")] DoNotContain,
+
+        /// 
+        ///     Should equal the given value.
+        /// 
+        [Description("Equals")] Equals
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterContext.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterContext.cs
new file mode 100644
index 00000000..f097d717
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterContext.cs
@@ -0,0 +1,21 @@
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Context filter.
+    /// 
+    public class FilterContext
+    {
+        /// 
+        ///     Ges the received error report
+        /// 
+        public ReportDTO ErrorReport { get; set; }
+
+        /// 
+        ///     Gets incident that the report was attached to.
+        /// 
+        public IncidentSummaryDTO Incident { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterResult.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterResult.cs
new file mode 100644
index 00000000..4facab30
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/FilterResult.cs
@@ -0,0 +1,31 @@
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Result for .
+    /// 
+    [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterResult")]
+    public enum FilterResult
+    {
+        /// 
+        ///     Rule did not match the given conditions.
+        /// 
+        NotMatched,
+
+        /// 
+        ///     Stop processing other rules and grant this report
+        /// 
+        Grant,
+
+        /// 
+        ///     Stop process other rules and revoke this report
+        /// 
+        Revoke,
+
+        /// 
+        ///     Ok by us, pass to any other roles.
+        /// 
+        Continue
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Filters/NamespaceDoc.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Filters/NamespaceDoc.cs
new file mode 100644
index 00000000..338994a0
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Filters/NamespaceDoc.cs
@@ -0,0 +1,12 @@
+using System.Runtime.CompilerServices;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Filters
+{
+    /// 
+    ///     Innehåller våra fördefinierade filter.
+    /// 
+    [CompilerGenerated]
+    internal class NamespaceDoc
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/ITriggerActionFactory.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/Actions/ITriggerActionFactory.cs
similarity index 81%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/ITriggerActionFactory.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/Actions/ITriggerActionFactory.cs
index 2cd4b4fe..fcc9adbe 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/ITriggerActionFactory.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/Actions/ITriggerActionFactory.cs
@@ -1,18 +1,19 @@
-using System;
-
-namespace OneTrueError.App.Modules.Triggers.Domain.Actions
-{
-    /// 
-    ///     used to create trigger actions by using their name
-    /// 
-    public interface ITriggerActionFactory
-    {
-        /// 
-        ///     Create a trigger
-        /// 
-        /// trigger name
-        /// trigger
-        /// no trigger have been mapped for that name.
-        ITriggerAction Create(string actionName);
-    }
+using System;
+using Coderr.Server.ReportAnalyzer.Triggers.Actions;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Handlers.Actions
+{
+    /// 
+    ///     used to create trigger actions by using their name
+    /// 
+    public interface ITriggerActionFactory
+    {
+        /// 
+        ///     Create a trigger
+        /// 
+        /// trigger name
+        /// trigger
+        /// no trigger have been mapped for that name.
+        ITriggerAction Create(string actionName);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/EventHandlers/TriggerFiltersOnReportAdded.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/TriggerFiltersOnReportAdded.cs
similarity index 80%
rename from src/Server/OneTrueError.App/Modules/Triggers/EventHandlers/TriggerFiltersOnReportAdded.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/TriggerFiltersOnReportAdded.cs
index a55cd4ba..53ecbf6b 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/EventHandlers/TriggerFiltersOnReportAdded.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/TriggerFiltersOnReportAdded.cs
@@ -1,83 +1,81 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using log4net;
-using OneTrueError.Api.Core.Incidents.Events;
-using OneTrueError.App.Modules.Triggers.Domain;
-using OneTrueError.App.Modules.Triggers.Domain.Actions;
-
-namespace OneTrueError.App.Modules.Triggers.EventHandlers
-{
-    /// 
-    ///     Waits on the ReportAdded and then loads all notifications for the application that the report belongs to.
-    /// 
-    [Component(RegisterAsSelf = true)]
-    public class TriggerFiltersOnReportAdded : IApplicationEventSubscriber
-    {
-        private readonly ITriggerActionFactory _actionFactory;
-        private readonly ITriggerRepository _repository;
-        private readonly ILog _logger = LogManager.GetLogger(typeof(TriggerFiltersOnReportAdded));
-
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// repos
-        /// repos
-        /// repository; actionFactory
-        public TriggerFiltersOnReportAdded(ITriggerRepository repository, ITriggerActionFactory actionFactory)
-        {
-            if (repository == null) throw new ArgumentNullException("repository");
-            if (actionFactory == null) throw new ArgumentNullException("actionFactory");
-            _repository = repository;
-            _actionFactory = actionFactory;
-        }
-
-        /// 
-        ///     Process an event asynchronously.
-        /// 
-        /// event to process
-        /// 
-        ///     Task to wait on.
-        /// 
-        public async Task HandleAsync(ReportAddedToIncident e)
-        {
-            _logger.Debug("doing filters..");
-            IEnumerable triggers;
-            try
-            {
-                triggers = _repository.GetForApplication(e.Incident.ApplicationId);
-            }
-            catch (Exception exception)
-            {
-                _logger.Error("Failed to load triggers for " + e.Incident.ApplicationId, exception);
-                return;
-            }
-
-            foreach (var trigger in triggers)
-            {
-                var triggerContext = new TriggerExecutionContext
-                {
-                    Incident = e.Incident,
-                    ErrorReport = e.Report
-                };
-                var triggerResults = trigger.Run(triggerContext);
-                foreach (var actionData in triggerResults)
-                {
-                    var action = _actionFactory.Create(actionData.ActionName);
-                    var context = new ActionExecutionContext
-                    {
-                        Config = actionData,
-                        ErrorReport = e.Report,
-                        Incident = e.Incident
-                    };
-                    await action.ExecuteAsync(context);
-                }
-            }
-
-            _logger.Debug("filters done..");
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using Coderr.Server.ReportAnalyzer.Triggers.Handlers.Actions;
+using DotNetCqs;
+using Coderr.Server.Abstractions.Boot;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Handlers
+{
+    /// 
+    ///     Waits on the ReportAdded and then loads all notifications for the application that the report belongs to.
+    /// 
+    public class TriggerFiltersOnReportAdded : IMessageHandler
+    {
+        private readonly ITriggerActionFactory _actionFactory;
+        private readonly ITriggerRepository _repository;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(TriggerFiltersOnReportAdded));
+
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repos
+        /// repos
+        /// repository; actionFactory
+        public TriggerFiltersOnReportAdded(ITriggerRepository repository, ITriggerActionFactory actionFactory)
+        {
+            if (repository == null) throw new ArgumentNullException("repository");
+            if (actionFactory == null) throw new ArgumentNullException("actionFactory");
+            _repository = repository;
+            _actionFactory = actionFactory;
+        }
+
+        /// 
+        ///     Process an event asynchronously.
+        /// 
+        /// event to process
+        /// 
+        ///     Task to wait on.
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            _logger.Debug("doing filters..");
+            IEnumerable triggers;
+            try
+            {
+                triggers = _repository.GetForApplication(e.Incident.ApplicationId);
+            }
+            catch (Exception exception)
+            {
+                _logger.Error("Failed to load triggers for " + e.Incident.ApplicationId, exception);
+                return;
+            }
+
+            foreach (var trigger in triggers)
+            {
+                var triggerContext = new TriggerExecutionContext
+                {
+                    Incident = e.Incident,
+                    ErrorReport = e.Report
+                };
+                var triggerResults = trigger.Run(triggerContext);
+                foreach (var actionData in triggerResults)
+                {
+                    var action = _actionFactory.Create(actionData.ActionName);
+                    var actionContext = new ActionExecutionContext
+                    {
+                        Config = actionData,
+                        ErrorReport = e.Report,
+                        Incident = e.Incident
+                    };
+                    await action.ExecuteAsync(actionContext);
+                }
+            }
+
+            _logger.Debug("filters done..");
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/UpdateCollectionsOnReportAdded.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/UpdateCollectionsOnReportAdded.cs
new file mode 100644
index 00000000..249cb51f
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Handlers/UpdateCollectionsOnReportAdded.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+using Coderr.Server.Abstractions.Boot;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Handlers
+{
+    /// 
+    ///     Responsible of creating context collection metadata for all reports that have been added to an incident.
+    /// 
+    public class UpdateCollectionsOnReportAdded : IMessageHandler
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(UpdateCollectionsOnReportAdded));
+        private readonly ITriggerRepository _repository;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repository
+        /// repository
+        public UpdateCollectionsOnReportAdded(ITriggerRepository repository)
+        {
+            _repository = repository ?? throw new ArgumentNullException(nameof(repository));
+        }
+
+        /// 
+        ///     Process an event asynchronously.
+        /// 
+        /// event to process
+        /// 
+        ///     Task to wait on.
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            if (e == null) throw new ArgumentNullException("e");
+
+            _logger.Debug("doing collections");
+            var collections = await _repository.GetCollectionsAsync(e.Incident.ApplicationId);
+            foreach (var collectionDto in e.Report.ContextCollections)
+            {
+                var isNew = false;
+                var meta =
+                    collections.FirstOrDefault(x => x.Name.Equals(collectionDto.Name, StringComparison.OrdinalIgnoreCase));
+                if (meta == null)
+                {
+                    isNew = true;
+                    meta = new CollectionMetadata(e.Incident.ApplicationId, collectionDto.Name);
+                }
+
+                foreach (var property in collectionDto.Properties)
+                {
+                    meta.AddOrUpdateProperty(property.Key);
+                }
+
+                if (!meta.IsUpdated)
+                    continue;
+
+                if (isNew)
+                    await _repository.CreateAsync(meta);
+                else
+                    await _repository.UpdateAsync(meta);
+            }
+
+            _logger.Debug("collections done");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ITriggerRepository.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ITriggerRepository.cs
new file mode 100644
index 00000000..463236ae
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ITriggerRepository.cs
@@ -0,0 +1,71 @@
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Griffin.Data;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Repository to load information for the Trigger root aggregate.
+    /// 
+    public interface ITriggerRepository
+    {
+        /// 
+        ///     Create a new trigger
+        /// 
+        /// trigger
+        /// task
+        Task CreateAsync(Trigger trigger);
+
+        /// 
+        ///     Create collection metadata
+        /// 
+        /// metadata
+        /// task
+        Task CreateAsync(CollectionMetadata collection);
+
+        /// 
+        ///     Delete a trigger
+        /// 
+        /// trigger PK
+        /// task
+        Task DeleteAsync(int id);
+
+        /// 
+        ///     Get a trigger
+        /// 
+        /// PK
+        /// trigger
+        /// Trigger was not found.
+        Task GetAsync(int id);
+
+        /// 
+        ///     Get collection metadata
+        /// 
+        /// application to load it for.
+        /// Metadata (or an empty collection)
+        [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
+        Task> GetCollectionsAsync(int applicationId);
+
+        /// 
+        ///     Get all triggers for the given application
+        /// 
+        /// app PK
+        /// 
+        IEnumerable GetForApplication(int applicationId);
+
+        /// 
+        ///     Update trigger
+        /// 
+        /// trigger
+        /// task
+        Task UpdateAsync(Trigger entity);
+
+        /// 
+        ///     Update metadata
+        /// 
+        /// collection
+        /// task
+        Task UpdateAsync(CollectionMetadata collection);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ITriggerRule.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ITriggerRule.cs
similarity index 84%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/ITriggerRule.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/ITriggerRule.cs
index ea7542cd..3b383026 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ITriggerRule.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ITriggerRule.cs
@@ -1,15 +1,15 @@
-namespace OneTrueError.App.Modules.Triggers.Domain
-{
-    /// 
-    ///     Decides if an error report can be passed on
-    /// 
-    public interface ITriggerRule
-    {
-        /// 
-        ///     Validate report
-        /// 
-        /// Context info
-        /// Recommendation
-        FilterResult Validate(FilterContext context);
-    }
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Decides if an error report can be passed on
+    /// 
+    public interface ITriggerRule
+    {
+        /// 
+        ///     Validate report
+        /// 
+        /// Context info
+        /// Recommendation
+        FilterResult Validate(FilterContext context);
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/LastTriggerAction.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/LastTriggerAction.cs
new file mode 100644
index 00000000..f32029ab
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/LastTriggerAction.cs
@@ -0,0 +1,18 @@
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     What to do if all filter rules have accepted the report.
+    /// 
+    public enum LastTriggerAction
+    {
+        /// 
+        ///     Grant actions execution.
+        /// 
+        Grant,
+
+        /// 
+        ///     Abort.
+        /// 
+        Revoke
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/ContextCollectionRule.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/ContextCollectionRule.cs
new file mode 100644
index 00000000..6653fa3c
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/ContextCollectionRule.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Rules
+{
+    /// 
+    ///     Check a context collection in the trigger
+    /// 
+    public class ContextCollectionRule : RuleBase, ITriggerRule
+    {
+        /// 
+        ///     Context collection to check
+        /// 
+        public string ContextName { get; set; }
+
+
+        /// 
+        ///     Property in that collection
+        /// 
+        public string PropertyName { get; set; }
+
+
+        /// 
+        ///     Value for the property
+        /// 
+        public string PropertyValue { get; set; }
+
+
+        /// 
+        ///     Validate report
+        /// 
+        /// Context info
+        /// Recommendation
+        public FilterResult Validate(FilterContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            if (string.IsNullOrEmpty(ContextName))
+            {
+                foreach (var ctx in context.ErrorReport.ContextCollections)
+                {
+                    if (Enumerable.Any>(ctx.Properties, property => Matches(PropertyValue, property.Value)))
+                    {
+                        return ResultToUse;
+                    }
+                }
+
+                return FilterResult.NotMatched;
+            }
+
+            var errContext =
+                Enumerable.FirstOrDefault(context.ErrorReport.ContextCollections, x => x.Name.Equals(ContextName, StringComparison.CurrentCultureIgnoreCase));
+            if (errContext == null)
+                return FilterResult.NotMatched;
+
+            var prop = Enumerable.Where>(errContext.Properties, x => x.Key.Equals(PropertyName, StringComparison.CurrentCultureIgnoreCase))
+                .Select(x => x.Value)
+                .FirstOrDefault();
+
+            if (prop == null)
+                return FilterResult.NotMatched;
+
+            return Matches(PropertyValue, prop) ? ResultToUse : FilterResult.NotMatched;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/ExceptionRule.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/ExceptionRule.cs
new file mode 100644
index 00000000..9d90c635
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/ExceptionRule.cs
@@ -0,0 +1,54 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Rules
+{
+    /// 
+    ///     Uses exception details (like Name, Message, StackTrace) to filter the trigger.
+    /// 
+    public class ExceptionRule : RuleBase, ITriggerRule
+    {
+        /// 
+        ///     Exception field name
+        /// 
+        public string FieldName { get; set; }
+
+        /// 
+        ///     Value to compare with
+        /// 
+        public string Value { get; set; }
+
+        /// 
+        ///     Validate report
+        /// 
+        /// Context info
+        /// Recommendation
+        public FilterResult Validate(FilterContext context)
+        {
+            if (context == null) throw new ArgumentNullException("context");
+            if (context.ErrorReport.Exception == null)
+                return FilterResult.NotMatched;
+
+            bool matches;
+            switch (FieldName)
+            {
+                case "Exception.Name":
+                    matches = Matches(Value, context.ErrorReport.Exception.Name);
+                    break;
+                case "Exception.Namespace":
+                    matches = Matches(Value, context.ErrorReport.Exception.Namespace);
+                    break;
+                case "Exception.Assembly":
+                    matches = Matches(Value, context.ErrorReport.Exception.AssemblyName);
+                    break;
+                case "Exception.StackTrace":
+                    matches = Matches(Value, context.ErrorReport.Exception.StackTrace);
+                    break;
+                default:
+                    throw new NotSupportedException(FieldName);
+            }
+
+
+            return matches ? ResultToUse : FilterResult.NotMatched;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/RuleBase.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/RuleBase.cs
new file mode 100644
index 00000000..6e7d953e
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Rules/RuleBase.cs
@@ -0,0 +1,47 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers.Rules
+{
+    /// 
+    ///     Base for trigger rules
+    /// 
+    public class RuleBase
+    {
+        /// 
+        ///     How to compare the values
+        /// 
+        public FilterCondition Condition { get; set; }
+
+        /// 
+        ///     Result to use if value comparison succeeds.
+        /// 
+        public FilterResult ResultToUse { get; set; }
+
+        /// 
+        ///     Match
+        /// 
+        /// first value
+        /// second value
+        /// true if values matches according to ; otherwise false.
+        public bool Matches(string value1, string value2)
+        {
+            if (value1 == null) throw new ArgumentNullException("value1");
+
+            switch (Condition)
+            {
+                case FilterCondition.Contains:
+                    return value1.IndexOf(value2, StringComparison.CurrentCultureIgnoreCase) > -1;
+                case FilterCondition.DoNotContain:
+                    return value1.IndexOf(value2, StringComparison.CurrentCultureIgnoreCase) == -1;
+                case FilterCondition.EndsWith:
+                    return value1.EndsWith(value2, StringComparison.CurrentCultureIgnoreCase);
+                case FilterCondition.StartsWith:
+                    return value1.StartsWith(value2, StringComparison.CurrentCultureIgnoreCase);
+                case FilterCondition.Equals:
+                    return value1.Equals(value2, StringComparison.CurrentCultureIgnoreCase);
+                default:
+                    throw new NotSupportedException(Condition.ToString());
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ServiceLocatorTriggerActionFactory.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ServiceLocatorTriggerActionFactory.cs
new file mode 100644
index 00000000..4eb11e4c
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/ServiceLocatorTriggerActionFactory.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.ReportAnalyzer.Triggers.Actions;
+using Coderr.Server.ReportAnalyzer.Triggers.Handlers.Actions;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Uses the IoC container to identify trigger actions
+    /// 
+    [ContainerService]
+    public class ServiceLocatorTriggerActionFactory : ITriggerActionFactory
+    {
+        private readonly IServiceProvider _serviceProvider;
+        private static readonly Dictionary _actionTypes = new Dictionary();
+
+        [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline",
+            Justification = "How on earth could I do that?")]
+        static ServiceLocatorTriggerActionFactory()
+        {
+            LoadTypes(Assembly.GetExecutingAssembly());
+        }
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// serviceLocator
+        public ServiceLocatorTriggerActionFactory(IServiceProvider serviceProvider)
+        {
+            _serviceProvider = serviceProvider;
+        }
+
+        /// 
+        ///     Create action
+        /// 
+        /// Name of the action to create
+        /// created action
+        /// Action is not supported
+        public ITriggerAction Create(string actionName)
+        {
+            if (!_actionTypes.TryGetValue(actionName, out var type))
+                throw new NotSupportedException("Do not support action of type " + actionName);
+
+            return (ITriggerAction) _serviceProvider.GetService(type);
+        }
+
+        /// 
+        ///     Find and load all trigger actions in the specified assembly
+        /// 
+        /// assembly to search
+        public static void LoadTypes(Assembly assembly)
+        {
+            if (assembly == null) throw new ArgumentNullException("assembly");
+
+            var types = from t in assembly.GetTypes()
+                where t.GetCustomAttribute() != null
+                select new {Type = t, Attrbibute = t.GetCustomAttribute()};
+            foreach (var pair in types)
+            {
+                _actionTypes.Add(pair.Attrbibute.Name, pair.Type);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Trigger.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Trigger.cs
new file mode 100644
index 00000000..15cbe7ad
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/Trigger.cs
@@ -0,0 +1,172 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     A filter which decides if a notification could be sent.
+    /// 
+    public class Trigger
+    {
+        private List _actions = new List();
+        private List _rules = new List();
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// application id
+        /// applicationId
+        public Trigger(int applicationId)
+        {
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId");
+            ApplicationId = applicationId;
+        }
+
+        /// 
+        ///     Serialization constructor
+        /// 
+        protected Trigger()
+        {
+        }
+
+        /// 
+        ///     Actions to take when the rules have been passed.
+        /// 
+        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Loaded by repos")]
+        public IEnumerable Actions
+        {
+            get { return _actions; }
+            private set { _actions = new List(value); }
+        }
+
+        /// 
+        ///     Application id
+        /// 
+        public int ApplicationId { get; set; }
+
+        /// 
+        ///     Why the trigger was created and what it does
+        /// 
+        public string Description { get; set; }
+
+
+        /// 
+        ///     Identity
+        /// 
+        public int Id { get; set; }
+
+        /// 
+        ///     If no filters match, do this.
+        /// 
+        public LastTriggerAction LastTriggerAction { get; set; }
+
+        /// 
+        ///     Trigger name
+        /// 
+        public string Name { get; set; }
+
+        /// 
+        ///     Rules to check
+        /// 
+        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Loaded by repos")]
+        public IEnumerable Rules
+        {
+            get { return _rules; }
+            private set { _rules = new List(value); }
+        }
+
+        /// 
+        ///     Run when we get a report for an existing incident.
+        /// 
+        public bool RunForExistingIncidents { get; set; }
+
+        /// 
+        ///     Should run for new incidents (receives a new unique exception)
+        /// 
+        public bool RunForNewIncidents { get; set; }
+
+        /// 
+        ///     Run for closed incidents that receive a new report.
+        /// 
+        public bool RunForReopenedIncidents { get; set; }
+
+        /// 
+        ///     Add a new action
+        /// 
+        /// what to do
+        public void AddAction(ActionConfigurationData actionData)
+        {
+            if (actionData == null) throw new ArgumentNullException("actionData");
+            _actions.Add(actionData);
+        }
+
+
+        /// 
+        ///     Add the rules in the order that they should be check in. the first rule added is the first rule that will decide
+        ///     which action to take.
+        /// 
+        /// Rule to add
+        public void AddRule(ITriggerRule rule)
+        {
+            if (rule == null) throw new ArgumentNullException("rule");
+
+            _rules.Add(rule);
+        }
+
+
+        /// 
+        ///     Remove all actions
+        /// 
+        public void RemoveActions()
+        {
+            _actions.Clear();
+        }
+
+        /// 
+        ///     Remove all rules.
+        /// 
+        public void RemoveRules()
+        {
+            _rules.Clear();
+        }
+
+        /// 
+        ///     Validate a new incoming report.
+        /// 
+        /// 
+        public IEnumerable Run(TriggerExecutionContext triggerContext)
+        {
+            if (triggerContext == null) throw new ArgumentNullException("triggerContext");
+
+            var incident = triggerContext.Incident;
+            var errorReport = triggerContext.ErrorReport;
+
+            var mayRun = RunForNewIncidents && incident.ReportCount == 1
+                         || RunForExistingIncidents && incident.ReportCount >= 1;
+            if (!mayRun)
+                return new ActionConfigurationData[0];
+
+
+            var isGranted = _rules.Count == 0;
+            var context = new FilterContext {ErrorReport = errorReport, Incident = incident};
+            foreach (var rule in _rules)
+            {
+                if (rule.Validate(context) == FilterResult.Revoke)
+                    return new ActionConfigurationData[0];
+
+                if (rule.Validate(context) == FilterResult.Grant)
+                {
+                    isGranted = true;
+                    break;
+                }
+            }
+
+            if (!isGranted && LastTriggerAction == LastTriggerAction.Revoke)
+                return new ActionConfigurationData[0];
+
+
+            return _actions.ToArray();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/TriggerActionNameAttribute.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/TriggerActionNameAttribute.cs
similarity index 91%
rename from src/Server/OneTrueError.App/Modules/Triggers/Domain/TriggerActionNameAttribute.cs
rename to src/Server/Coderr.Server.ReportAnalyzer/Triggers/TriggerActionNameAttribute.cs
index df4fae38..c4afac44 100644
--- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/TriggerActionNameAttribute.cs
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/TriggerActionNameAttribute.cs
@@ -1,27 +1,27 @@
-using System;
-
-namespace OneTrueError.App.Modules.Triggers.Domain
-{
-    /// 
-    ///     Used to be able to create instances of trigger actions (from when we load configuration data from the data source)
-    /// 
-    [AttributeUsage(AttributeTargets.Class)]
-    public sealed class TriggerActionNameAttribute : Attribute
-    {
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// action name
-        /// name
-        public TriggerActionNameAttribute(string name)
-        {
-            if (name == null) throw new ArgumentNullException("name");
-            Name = name;
-        }
-
-        /// 
-        ///     Action name
-        /// 
-        public string Name { get; private set; }
-    }
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Used to be able to create instances of trigger actions (from when we load configuration data from the data source)
+    /// 
+    [AttributeUsage(AttributeTargets.Class)]
+    public sealed class TriggerActionNameAttribute : Attribute
+    {
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// action name
+        /// name
+        public TriggerActionNameAttribute(string name)
+        {
+            if (name == null) throw new ArgumentNullException("name");
+            Name = name;
+        }
+
+        /// 
+        ///     Action name
+        /// 
+        public string Name { get; private set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/Triggers/TriggerExecutionContext.cs b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/TriggerExecutionContext.cs
new file mode 100644
index 00000000..c2d7ea68
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/Triggers/TriggerExecutionContext.cs
@@ -0,0 +1,21 @@
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+
+namespace Coderr.Server.ReportAnalyzer.Triggers
+{
+    /// 
+    ///     Context providing when the trigger should execute
+    /// 
+    public class TriggerExecutionContext
+    {
+        /// 
+        ///     Report that triggered the trigger (ha ha)
+        /// 
+        public ReportDTO ErrorReport { get; set; }
+
+        /// 
+        ///     Incident that the received report belongs to
+        /// 
+        public IncidentSummaryDTO Incident { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/BrowserNotificationConfig.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/BrowserNotificationConfig.cs
new file mode 100644
index 00000000..27fdef02
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/BrowserNotificationConfig.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using Coderr.Server.Abstractions.Config;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications
+{
+    /// 
+    ///     Configuration settings for browser notifications.
+    /// 
+    public class BrowserNotificationConfig : IConfigurationSection//, IReportConfig
+    {
+        /// 
+        ///     Creates a new instance of 
+        /// 
+        public BrowserNotificationConfig()
+        {
+        }
+
+        public string PublicKey { get; set; }
+
+        public string PrivateKey { get; set; }
+
+        /// 
+        ///     Number of days to store reports.
+        /// 
+        /// 
+        ///     All reports older than this amount of days will be deleted.
+        /// 
+        public int RetentionDays { get; set; }
+
+        string IConfigurationSection.SectionName => "BrowserNotificationConfig";
+
+        IDictionary IConfigurationSection.ToDictionary()
+        {
+            return this.ToConfigDictionary();
+        }
+
+        void IConfigurationSection.Load(IDictionary settings)
+        {
+            this.AssignProperties(settings);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Dtos/Notification.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Dtos/Notification.cs
new file mode 100644
index 00000000..5a7c7093
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Dtos/Notification.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications.Dtos
+{
+    /// 
+    ///     Notification API Standard
+    /// 
+    public class Notification
+    {
+        public Notification()
+        {
+        }
+
+        public Notification(string text)
+        {
+            Body = text;
+        }
+
+        [JsonProperty("actions")]
+        public List Actions { get; set; } =
+            new List();
+
+        [JsonProperty("data", TypeNameHandling = TypeNameHandling.None)]
+        public object Data { get; set; }
+
+        [JsonProperty("badge")] public string Badge { get; set; }
+
+        [JsonProperty("body")] public string Body { get; set; }
+
+        [JsonProperty("icon")] public string Icon { get; set; }
+
+        [JsonProperty("image")] public string Image { get; set; }
+
+        [JsonProperty("lang")] public string Lang { get; set; } = "en";
+
+        [JsonProperty("requireInteraction")] public bool RequireInteraction { get; set; }
+
+        [JsonProperty("tag")] public string Tag { get; set; }
+
+        [JsonProperty("timestamp")] public DateTime Timestamp { get; set; } = DateTime.Now;
+
+        [JsonProperty("title")] public string Title { get; set; } = "Push Demo";
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Dtos/NotificationAction.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Dtos/NotificationAction.cs
new file mode 100644
index 00000000..549462e4
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Dtos/NotificationAction.cs
@@ -0,0 +1,17 @@
+using Newtonsoft.Json;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications.Dtos
+{
+    /// 
+    ///     Notification API Standard
+    /// 
+    public class NotificationAction
+    {
+
+        [JsonProperty("action")]
+        public string Action { get; set; }
+
+        [JsonProperty("title")]
+        public string Title { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/CheckForNotificationsToSendOnReportReceived.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/CheckForNotificationsToSendOnReportReceived.cs
new file mode 100644
index 00000000..d8dc1779
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/CheckForNotificationsToSendOnReportReceived.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.User;
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Coderr.Server.Infrastructure.Configuration;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Handlers.Tasks;
+using DotNetCqs;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications.Handlers
+{
+    /// 
+    ///     Responsible of sending notifications when a new report have been analyzed.
+    /// 
+    public class CheckForNotificationsToSendOnReportReceived : IMessageHandler
+    {
+        private readonly IUserNotificationsRepository _notificationsRepository;
+        private readonly IUserRepository _userRepository;
+        private readonly BaseConfiguration _configuration;
+        private readonly ILog _log = LogManager.GetLogger(typeof(CheckForNotificationsToSendOnReportReceived));
+        private readonly INotificationService _notificationService;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// To load notification configuration
+        /// To load user info
+        public CheckForNotificationsToSendOnReportReceived(IUserNotificationsRepository notificationsRepository,
+            IUserRepository userRepository, IConfiguration configuration, INotificationService notificationService)
+        {
+            _notificationsRepository = notificationsRepository;
+            _userRepository = userRepository;
+            _notificationService = notificationService;
+            _configuration = configuration.Value;
+        }
+
+        /// 
+        ///     Process an event asynchronously.
+        /// 
+        /// event to process
+        /// 
+        ///     Task to wait on.
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            if (e == null) throw new ArgumentNullException(nameof(e));
+
+            _log.Info("ReportId: " + e.Report.Id);
+
+            var settings = await _notificationsRepository.GetAllAsync(e.Incident.ApplicationId);
+            foreach (var setting in settings)
+            {
+                if (setting.NewIncident != NotificationState.Disabled && e.IsNewIncident == true)
+                {
+                    if (string.IsNullOrEmpty(e.EnvironmentName)
+                    || e.EnvironmentName.Equals("production", StringComparison.OrdinalIgnoreCase)
+                        || e.EnvironmentName.Equals("prod", StringComparison.OrdinalIgnoreCase))
+                    {
+                        await CreateNotification(context, e, setting.AccountId, setting.NewIncident);
+                    }
+                    else
+                    {
+                        _log.Debug("Error was new, but not for the production environment: " + e.EnvironmentName);
+                    }
+                }
+                else if (setting.ReopenedIncident != NotificationState.Disabled && e.IsReOpened)
+                {
+                    await CreateNotification(context, e, setting.AccountId, setting.ReopenedIncident);
+                }
+            }
+        }
+
+        private async Task CreateNotification(IMessageContext context, ReportAddedToIncident e, int accountId,
+            NotificationState state)
+        {
+            if (state == NotificationState.BrowserNotification)
+            {
+                var notification = new Notification($"Application: {e.Incident.ApplicationName}\r\n{e.Incident.Name}");
+                notification.Actions.Add(new NotificationAction()
+                {
+                    Title = "Assign to me",
+                    Action = "AssignToMe"
+                });
+                notification.Actions.Add(new NotificationAction()
+                {
+                    Title = "View",
+                    Action = "View"
+                });
+                notification.Icon = "/favicon-32x32.png";
+                notification.Timestamp = e.Report.CreatedAtUtc;
+                notification.Title = e.IsNewIncident == true
+                    ? "New incident"
+                    : "Re-opened incident";
+                notification.Data = new
+                {
+                    applicationId = e.Incident.ApplicationId,
+                    incidentId = e.Incident.Id
+                };
+                await _notificationService.SendBrowserNotification(accountId, notification);
+            }
+            else if (state == NotificationState.Email)
+            {
+                var email = new SendIncidentEmail(_configuration);
+                await email.SendAsync(context, accountId.ToString(), e.Incident, e.Report);
+            }
+            else
+            {
+                var handler = new SendIncidentSms(_userRepository, _configuration);
+                await handler.SendAsync(accountId, e.Incident, e.Report);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/CheckForNotificationsToSendOnReportRecevied.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/CheckForNotificationsToSendOnReportRecevied.cs
new file mode 100644
index 00000000..d203c0cb
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/CheckForNotificationsToSendOnReportRecevied.cs
@@ -0,0 +1,117 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.User;
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Coderr.Server.Infrastructure.Configuration;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Handlers.Tasks;
+using DotNetCqs;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos;
+using log4net;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications.Handlers
+{
+    /// 
+    ///     Responsible of sending notifications when a new report have been analyzed.
+    /// 
+    public class CheckForNotificationsToSendOnReportRecevied : IMessageHandler
+    {
+        private readonly IUserNotificationsRepository _notificationsRepository;
+        private readonly IUserRepository _userRepository;
+        private readonly BaseConfiguration _configuration;
+        private readonly ILog _log = LogManager.GetLogger(typeof(CheckForNotificationsToSendOnReportRecevied));
+        private INotificationService _notificationService;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// To load notification configuration
+        /// To load user info
+        public CheckForNotificationsToSendOnReportRecevied(IUserNotificationsRepository notificationsRepository,
+            IUserRepository userRepository, IConfiguration configuration, INotificationService notificationService)
+        {
+            _notificationsRepository = notificationsRepository;
+            _userRepository = userRepository;
+            _notificationService = notificationService;
+            _configuration = configuration.Value;
+        }
+
+        /// 
+        ///     Process an event asynchronously.
+        /// 
+        /// event to process
+        /// 
+        ///     Task to wait on.
+        /// 
+        public async Task HandleAsync(IMessageContext context, ReportAddedToIncident e)
+        {
+            if (e == null) throw new ArgumentNullException(nameof(e));
+
+            _log.Info("ReportId: " + e.Report.Id);
+
+            var settings = await _notificationsRepository.GetAllAsync(e.Incident.ApplicationId);
+            foreach (var setting in settings)
+            {
+                if (setting.NewIncident != NotificationState.Disabled && e.IsNewIncident == true)
+                {
+                    if (string.IsNullOrEmpty(e.EnvironmentName)
+                    || e.EnvironmentName.Equals("production", StringComparison.OrdinalIgnoreCase)
+                        || e.EnvironmentName.Equals("prod", StringComparison.OrdinalIgnoreCase))
+                    {
+                        await CreateNotification(context, e, setting.AccountId, setting.NewIncident);
+                    }
+                    else
+                    {
+                        _log.Debug("Error was new, but not for the production environment: " + e.EnvironmentName);
+                    }
+                }
+                else if (setting.ReopenedIncident != NotificationState.Disabled && e.IsReOpened)
+                {
+                    await CreateNotification(context, e, setting.AccountId, setting.ReopenedIncident);
+                }
+            }
+        }
+
+        private async Task CreateNotification(IMessageContext context, ReportAddedToIncident e, int accountId,
+            NotificationState state)
+        {
+            if (state == NotificationState.BrowserNotification)
+            {
+                var notification = new Notification($"Application: {e.Incident.ApplicationName}\r\n{e.Incident.Name}");
+                notification.Actions.Add(new NotificationAction()
+                {
+                    Title = "Assign to me",
+                    Action = "AssignToMe"
+                });
+                notification.Actions.Add(new NotificationAction()
+                {
+                    Title = "View",
+                    Action = "View"
+                });
+                notification.Icon = "/favicon-32x32.png";
+                notification.Timestamp = e.Report.CreatedAtUtc;
+                notification.Title = e.IsNewIncident == true
+                    ? "New incident"
+                    : "Re-opened incident";
+                notification.Data = new
+                {
+                    applicationId = e.Incident.ApplicationId,
+                    incidentId = e.Incident.Id
+                };
+                await _notificationService.SendBrowserNotification(accountId, notification);
+            }
+            else if (state == NotificationState.Email)
+            {
+                var email = new SendIncidentEmail(_configuration);
+                await email.SendAsync(context, accountId.ToString(), e.Incident, e.Report);
+            }
+            else
+            {
+                var handler = new SendIncidentSms(_userRepository, _configuration);
+                await handler.SendAsync(accountId, e.Incident, e.Report);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/SendBrowserNotificationHandler.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/SendBrowserNotificationHandler.cs
new file mode 100644
index 00000000..ba1e962b
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/SendBrowserNotificationHandler.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.ReportAnalyzer.Abstractions.Notifications.Commands;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications.Handlers
+{
+    internal class SendBrowserNotificationHandler : IMessageHandler
+    {
+        private readonly INotificationService _notificationService;
+
+        public SendBrowserNotificationHandler(INotificationService notificationService)
+        {
+            _notificationService = notificationService;
+        }
+
+        public async Task HandleAsync(IMessageContext context, SendBrowserNotification message)
+        {
+            var notification = new Notification
+            {
+                Actions = message.Actions.Select(ConvertAction).ToList(),
+                Badge = message.Badge,
+                Body = message.Body,
+                Data = message.UserData,
+                Icon = message.IconUrl,
+                Image = message.ImageUrl,
+                Lang = message.LanguageCode,
+                RequireInteraction = message.RequireInteraction,
+                Tag = message.Tag,
+                Timestamp = message.Timestamp,
+                Title = message.Title
+            };
+            await _notificationService.SendBrowserNotification(message.AccountIdToSendTo, notification);
+        }
+
+        private NotificationAction ConvertAction(SendBrowserNotificationAction arg)
+        {
+            return new NotificationAction {Title = arg.Title, Action = arg.Action};
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/Tasks/SendIncidentEmail.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/Tasks/SendIncidentEmail.cs
new file mode 100644
index 00000000..f7bc6a74
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/Tasks/SendIncidentEmail.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Messaging;
+using Coderr.Server.Api.Core.Messaging.Commands;
+using Coderr.Server.Infrastructure.Configuration;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using DotNetCqs;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications.Handlers.Tasks
+{
+    /// 
+    ///     Send incident email
+    /// 
+    public class SendIncidentEmail
+    {
+        private readonly BaseConfiguration _baseConfiguration;
+
+        public SendIncidentEmail(BaseConfiguration baseConfiguration)
+        {
+            _baseConfiguration = baseConfiguration;
+        }
+
+        /// 
+        ///     Send
+        /// 
+        /// Account id or email address
+        /// Incident that the report belongs to
+        /// Report being processed.
+        /// task
+        /// idOrEmailAddress; incident; report
+        public async Task SendAsync(IMessageContext context, string idOrEmailAddress, IncidentSummaryDTO incident,
+            ReportDTO report)
+        {
+            if (idOrEmailAddress == null) throw new ArgumentNullException("idOrEmailAddress");
+            if (incident == null) throw new ArgumentNullException("incident");
+            if (report == null) throw new ArgumentNullException("report");
+
+            var shortName = incident.Name.Length > 40
+                ? incident.Name.Substring(0, 40) + "..."
+                : incident.Name;
+
+            var pos = shortName.IndexOfAny(new[] {'\r', '\n'});
+            if (pos != -1) shortName = shortName.Substring(0, pos) + "[...]";
+
+
+            var baseUrl = _baseConfiguration.BaseUrl.ToString().TrimEnd('/');
+            var incidentUrl =
+                $"{baseUrl}/discover/incidents/{report.ApplicationId}/incident/{report.IncidentId}/";
+
+            //TODO: Add more information
+            var msg = new EmailMessage(idOrEmailAddress);
+            if (incident.IsReOpened)
+            {
+                msg.Subject = "ReOpened: " + shortName;
+                msg.TextBody = $@"{incident.Name}
+{report.Exception.FullName}
+{report.Exception.StackTrace}
+
+{incidentUrl}";
+            }
+            else if (incident.ReportCount == 1)
+            {
+                msg.Subject = "New: " + shortName;
+                msg.TextBody = $@"{incident.Name}
+{report.Exception.FullName}
+{report.Exception.StackTrace}
+
+{incidentUrl}";
+            }
+            else
+            {
+                msg.Subject = "Updated: " + shortName;
+                msg.TextBody = $@"{incident.Name}
+{report.Exception.FullName}
+{report.Exception.StackTrace}
+
+{incidentUrl}";
+            }
+
+            var emailCmd = new SendEmail(msg);
+            await context.SendAsync(emailCmd);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/Tasks/SendIncidentSms.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/Tasks/SendIncidentSms.cs
new file mode 100644
index 00000000..95450bb6
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/Handlers/Tasks/SendIncidentSms.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Net;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.User;
+using Coderr.Server.Infrastructure.Configuration;
+using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications.Handlers.Tasks
+{
+    /// 
+    ///     Send SMS regarding an incident
+    /// 
+    public class SendIncidentSms
+    {
+        private readonly IUserRepository _userRepository;
+        private readonly BaseConfiguration _baseConfiguration;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// to fetch phone number
+        /// 
+        /// userRepository
+        public SendIncidentSms(IUserRepository userRepository, BaseConfiguration baseConfiguration)
+        {
+            if (userRepository == null) throw new ArgumentNullException("userRepository");
+            _userRepository = userRepository;
+            _baseConfiguration = baseConfiguration;
+        }
+
+        /// 
+        ///     Send
+        /// 
+        /// Account to send to
+        /// Incident that the report belongs to
+        /// report being processed
+        /// task
+        /// incident;report
+        /// accountId
+        public async Task SendAsync(int accountId, IncidentSummaryDTO incident, ReportDTO report)
+        {
+            if (incident == null) throw new ArgumentNullException("incident");
+            if (report == null) throw new ArgumentNullException("report");
+            if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId");
+
+            var settings = await _userRepository.GetUserAsync(accountId);
+            if (string.IsNullOrEmpty(settings.MobileNumber))
+                return; //TODO: LOG
+
+            var url = _baseConfiguration.BaseUrl;
+            var shortName = incident.Name.Length > 20
+                ? incident.Name.Substring(0, 20) + "..."
+                : incident.Name;
+
+            var exMsg = report.Exception.Message.Length > 100
+                ? report.Exception.Message.Substring(0, 100)
+                : report.Exception.Message;
+
+
+            var baseUrl = _baseConfiguration.BaseUrl.ToString().TrimEnd('/');
+            var incidentUrl =
+                $"{baseUrl}/discover/incidents/{report.ApplicationId}/incident/{report.IncidentId}/";
+
+            string msg;
+            if (incident.IsReOpened)
+            {
+                msg = $@"ReOpened: {shortName}
+{incidentUrl}
+
+{exMsg}";
+            }
+            else if (incident.ReportCount == 1)
+            {
+                msg = $@"New: {shortName}
+{incidentUrl}
+
+{exMsg}";
+            }
+            else
+            {
+                msg = $@"Updated: {shortName}
+ReportCount: {incident.ReportCount}
+{incidentUrl}
+
+{exMsg}";
+            }
+
+            var iso = Encoding.GetEncoding("ISO-8859-1");
+            var utfBytes = Encoding.UTF8.GetBytes(msg);
+            var isoBytes = Encoding.Convert(Encoding.UTF8, iso, utfBytes);
+            msg = iso.GetString(isoBytes);
+
+            var request =
+                WebRequest.CreateHttp("https://web.smscom.se/sendsms.aspx?acc=ip1-755&pass=z35llww4&msg=" +
+                                      Uri.EscapeDataString(msg) + "&to=" + settings.MobileNumber +
+                                      "&from=Coderr&prio=2");
+            request.ContentType = "application/json";
+            request.Method = "GET";
+            await request.GetResponseAsync();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/INotificationService.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/INotificationService.cs
new file mode 100644
index 00000000..41dfb4df
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/INotificationService.cs
@@ -0,0 +1,19 @@
+using System.Threading.Tasks;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications
+{
+    /// 
+    ///     Used to send notifications to users.
+    /// 
+    public interface INotificationService
+    {
+        /// 
+        ///     Send a browser notification (requires that the user first have approved notifications through javascript).
+        /// 
+        /// Account to send to
+        /// Notification details
+        /// 
+        Task SendBrowserNotification(int accountId, Notification notification);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/IWebPushClient.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/IWebPushClient.cs
new file mode 100644
index 00000000..549fa496
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/IWebPushClient.cs
@@ -0,0 +1,20 @@
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications
+{
+    /// 
+    ///     Abstraction for the web push implementation.
+    /// 
+    public interface IWebPushClient
+    {
+        /// 
+        ///     Send the notification to the push service of the browser.
+        /// 
+        /// User subscription
+        /// Information to send.
+        /// 
+        Task SendNotification(BrowserSubscription subscription, Notification notification);
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/InvalidSubscriptionException.cs b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/InvalidSubscriptionException.cs
new file mode 100644
index 00000000..f85a6529
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/UserNotifications/InvalidSubscriptionException.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Coderr.Server.ReportAnalyzer.UserNotifications
+{
+    /// 
+    ///     Browser subscription is not valid (remove it).
+    /// 
+    /// 
+    ///     
+    ///         TODO: Notify the user that it was removed.
+    ///     
+    /// 
+    public class InvalidSubscriptionException : Exception
+    {
+        /// 
+        public InvalidSubscriptionException(string errorMessage, Exception inner)
+            : base(errorMessage, inner)
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.ReportAnalyzer/codeRR.Server.ReportAnalyzer.csproj.DotSettings b/src/Server/Coderr.Server.ReportAnalyzer/codeRR.Server.ReportAnalyzer.csproj.DotSettings
new file mode 100644
index 00000000..c54c126d
--- /dev/null
+++ b/src/Server/Coderr.Server.ReportAnalyzer/codeRR.Server.ReportAnalyzer.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+	CSharp70
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Analysis/IncidentBeingAnalyzedMapper.cs b/src/Server/Coderr.Server.SqlServer.Tests/Analysis/IncidentBeingAnalyzedMapper.cs
new file mode 100644
index 00000000..05317add
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Analysis/IncidentBeingAnalyzedMapper.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Incidents;
+using Coderr.Server.SqlServer.ReportAnalyzer;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Coderr.Server.SqlServer.Tests.Analysis
+{
+    public class IncidentBeingAnalyzedMapperTests : IntegrationTest
+    {
+        public IncidentBeingAnalyzedMapperTests(ITestOutputHelper helper) : base(helper)
+        {
+            helper.WriteLine("Hello world");
+            ResetDatabase();
+        }
+
+        [Fact]
+        public void Should_load_ignored_state_into_class_correctly()
+        {
+            var report = new ErrorReportEntity(FirstApplicationId, Guid.NewGuid().ToString("N"), DateTime.UtcNow,
+                new ErrorReportException(new Exception("mofo")),
+                new List {new ErrorReportContextCollection("Maps", new Dictionary())})
+            {
+                Title = "Missing here"
+            };
+            report.Init(report.GenerateHashCodeIdentifier());
+
+            using (var uow = CreateUnitOfWork())
+            {
+                var incident = new IncidentBeingAnalyzed(report);
+                var incRepos = new AnalyticsRepository(uow);
+                incRepos.CreateIncident(incident);
+                report.IncidentId = incident.Id;
+                incRepos.CreateReport(report);
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Coderr.Server.SqlServer.Tests.csproj b/src/Server/Coderr.Server.SqlServer.Tests/Coderr.Server.SqlServer.Tests.csproj
new file mode 100644
index 00000000..0737f7d1
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Coderr.Server.SqlServer.Tests.csproj
@@ -0,0 +1,36 @@
+
+  
+    netcoreapp3.1
+    true
+    true
+    Debug;Release;Premise
+  
+
+  
+    
+      PreserveNewest
+    
+  
+
+  
+    
+    
+    
+    
+    
+    
+    
+    
+      all
+      runtime; build; native; contentfiles; analyzers; buildtransitive
+    
+  
+  
+    
+    
+    
+    
+  
+
+
+
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Core/Accounts/AccountRepositoryTests.cs b/src/Server/Coderr.Server.SqlServer.Tests/Core/Accounts/AccountRepositoryTests.cs
new file mode 100644
index 00000000..91ca62ed
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Core/Accounts/AccountRepositoryTests.cs
@@ -0,0 +1,40 @@
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.Account;
+using Coderr.Server.SqlServer.Core.Accounts;
+using Coderr.Server.SqlServer.Tests;
+using FluentAssertions;
+using Griffin.Data;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Coderr.Server.SqlServer.Tests.Core.Accounts
+{
+    public class AccountRepositoryTests : IntegrationTest
+    {
+
+        public AccountRepositoryTests(ITestOutputHelper helper) : base(helper)
+        {
+        }
+
+
+        [Fact]
+        public async Task Should_include_id_when_specified_in_the_account_entity()
+        {
+            return; //TODO: Requires a modified version of the DB (since Lobby generates accounts in Coderr Live)
+            var account = new Account(43, "arne", "pass");
+            using (var uow = CreateUnitOfWork())
+            {
+                var repos = new AccountRepository(uow);
+                await repos.CreateAsync(account);
+                uow.SaveChanges();
+            }
+
+
+            using (var uow2 = CreateUnitOfWork())
+            {
+                var repos2 = new AccountRepository(uow2);
+                await repos2.GetByIdAsync(43);
+            }
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Commands/CreateApiKeyHandlerTests.cs b/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Commands/CreateApiKeyHandlerTests.cs
new file mode 100644
index 00000000..213e08e4
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Commands/CreateApiKeyHandlerTests.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.ApiKeys.Commands;
+using Coderr.Server.Domain.Core.Applications;
+using Coderr.Server.SqlServer.Core.ApiKeys;
+using Coderr.Server.SqlServer.Core.ApiKeys.Commands;
+using Coderr.Server.SqlServer.Core.Applications;
+using DotNetCqs;
+using FluentAssertions;
+using Griffin.Data;
+using NSubstitute;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Coderr.Server.SqlServer.Tests.Core.ApiKeys.Commands
+{
+
+    public class CreateApiKeyHandlerTests : IntegrationTest
+    {
+        private int _applicationId;
+
+        public CreateApiKeyHandlerTests(ITestOutputHelper helper) : base(helper)
+        {
+            GetApplicationId();
+        }
+
+        [Fact]
+        public async Task Should_be_able_to_Create_a_key_without_applications_mapped()
+        {
+            var cmd = new CreateApiKey("Mofo", Guid.NewGuid().ToString("N"), Guid.NewGuid().ToString("N"));
+            var bus = Substitute.For();
+            var ctx = Substitute.For();
+
+            using (var uow = CreateUnitOfWork())
+            {
+                var sut = new CreateApiKeyHandler(uow, bus);
+                await sut.HandleAsync(ctx, cmd);
+
+                var repos = new ApiKeyRepository(uow);
+                var generated = await repos.GetByKeyAsync(cmd.ApiKey);
+                generated.Should().NotBeNull();
+                generated.Claims.Should().NotBeEmpty("because keys without appIds are universal");
+
+                uow.SaveChanges();
+            }
+
+        }
+
+        [Fact]
+        public async Task Should_be_able_to_Create_key()
+        {
+            var cmd = new CreateApiKey("Mofo", Guid.NewGuid().ToString("N"), Guid.NewGuid().ToString("N"),
+                new[] { _applicationId });
+            var bus = Substitute.For();
+            var ctx = Substitute.For();
+
+            using (var uow = CreateUnitOfWork())
+            {
+
+                var sut = new CreateApiKeyHandler(uow, bus);
+                await sut.HandleAsync(ctx, cmd);
+
+                var repos = new ApiKeyRepository(uow);
+                var generated = await repos.GetByKeyAsync(cmd.ApiKey);
+                generated.Should().NotBeNull();
+                generated.Claims.First().Value.Should().BeEquivalentTo(_applicationId.ToString());
+                uow.SaveChanges();
+            }
+        }
+
+        private void GetApplicationId()
+        {
+            if (_applicationId != 0)
+                return;
+
+            using (var uow = CreateUnitOfWork())
+            {
+
+                var repos = new ApplicationRepository(uow);
+                var id = uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications WITH (ReadPast)");
+                if (id is DBNull || id is null)
+                {
+                    repos.CreateAsync(new Application(10, "AppTen")).GetAwaiter().GetResult();
+                    _applicationId = (int)uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications WITH (ReadPast)");
+                }
+                else
+                    _applicationId = (int)id;
+                uow.SaveChanges();
+            }
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Commands/DeleteApiKeyHandlerTests.cs b/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Commands/DeleteApiKeyHandlerTests.cs
new file mode 100644
index 00000000..4b9b1b48
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Commands/DeleteApiKeyHandlerTests.cs
@@ -0,0 +1,104 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.ApiKeys.Commands;
+using Coderr.Server.App.Core.ApiKeys;
+using Coderr.Server.Domain.Core.Applications;
+using Coderr.Server.SqlServer.Core.ApiKeys;
+using Coderr.Server.SqlServer.Core.ApiKeys.Commands;
+using Coderr.Server.SqlServer.Core.Applications;
+using DotNetCqs;
+using FluentAssertions;
+using Griffin.Data;
+using NSubstitute;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Coderr.Server.SqlServer.Tests.Core.ApiKeys.Commands
+{
+
+    public class DeleteApiKeyHandlerTests : IntegrationTest
+    {
+        private int _applicationId;
+        private readonly ApiKey _existingEntity;
+
+        public DeleteApiKeyHandlerTests(ITestOutputHelper helper) : base(helper)
+        {
+            GetApplicationId();
+
+            _existingEntity = new ApiKey
+            {
+                ApplicationName = "Arne",
+                GeneratedKey = Guid.NewGuid().ToString("N"),
+                SharedSecret = Guid.NewGuid().ToString("N"),
+                CreatedById = 20,
+                CreatedAtUtc = DateTime.UtcNow
+            };
+
+            _existingEntity.Add(_applicationId);
+            using (var uow = CreateUnitOfWork())
+            {
+                var repos = new ApiKeyRepository(uow);
+                repos.CreateAsync(_existingEntity).GetAwaiter().GetResult();
+                uow.SaveChanges();
+            }
+        }
+        [Fact]
+        public async Task Should_be_able_to_delete_key_by_ApiKey()
+        {
+            var cmd = new DeleteApiKey(_existingEntity.GeneratedKey);
+            var context = Substitute.For();
+
+            using (var uow = CreateUnitOfWork())
+            {
+                var sut = new DeleteApiKeyHandler(uow);
+                await sut.HandleAsync(context, cmd);
+
+                var count = uow.ExecuteScalar("SELECT cast(count(*) as int) FROM ApiKeys WHERE Id = @id",
+                    new { id = _existingEntity.Id });
+                count.Should().Be(0);
+                uow.SaveChanges();
+            }
+
+        }
+
+        [Fact]
+        public async Task Should_be_able_to_delete_key_by_id()
+        {
+            var cmd = new DeleteApiKey(_existingEntity.Id);
+            var context = Substitute.For();
+
+            using (var uow = CreateUnitOfWork())
+            {
+                var sut = new DeleteApiKeyHandler(uow);
+                await sut.HandleAsync(context, cmd);
+
+                var count = uow.ExecuteScalar("SELECT cast(count(*) as int) FROM ApiKeys WHERE Id = @id",
+                    new { id = _existingEntity.Id });
+                count.Should().Be(0);
+                uow.SaveChanges();
+            }
+
+        }
+
+        private void GetApplicationId()
+        {
+            if (_applicationId != 0)
+                return;
+
+            using (var uow = CreateUnitOfWork())
+            {
+                var repos = new ApplicationRepository(uow);
+                var id = uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications WITH (ReadPast)");
+                if (id is DBNull || id is null)
+                {
+                    repos.CreateAsync(new Application(10, "AppTen")).Wait();
+                    _applicationId = (int)uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications WITH (ReadPast)");
+                }
+                else
+                    _applicationId = (int)id;
+                uow.SaveChanges();
+            }
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Queries/GetApiKeyHandlerTests.cs b/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Queries/GetApiKeyHandlerTests.cs
new file mode 100644
index 00000000..a56285bb
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Queries/GetApiKeyHandlerTests.cs
@@ -0,0 +1,91 @@
+using System;
+using Coderr.Server.Api.Core.ApiKeys.Queries;
+using Coderr.Server.App.Core.ApiKeys;
+using Coderr.Server.Domain.Core.Applications;
+using Coderr.Server.SqlServer.Core.ApiKeys;
+using Coderr.Server.SqlServer.Core.ApiKeys.Queries;
+using Coderr.Server.SqlServer.Core.Applications;
+using DotNetCqs;
+using FluentAssertions;
+using Griffin.Data;
+using NSubstitute;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Coderr.Server.SqlServer.Tests.Core.ApiKeys.Queries
+{
+    public class GetApiKeyHandlerTests : IntegrationTest
+    {
+        private Application _application;
+        private readonly ApiKey _existingEntity;
+
+        public GetApiKeyHandlerTests(ITestOutputHelper helper):base(helper)
+        {
+            GetApplication();
+
+            _existingEntity = new ApiKey
+            {
+                ApplicationName = "Arne",
+                GeneratedKey = Guid.NewGuid().ToString("N"),
+                SharedSecret = Guid.NewGuid().ToString("N"),
+                CreatedById = 20,
+                CreatedAtUtc = DateTime.UtcNow
+            };
+
+            _existingEntity.Add(_application.Id);
+            using (var uow = CreateUnitOfWork())
+            {
+                var repos = new ApiKeyRepository(uow);
+                repos.CreateAsync(_existingEntity).GetAwaiter().GetResult();
+                uow.SaveChanges();
+            }
+
+        }
+
+
+
+        [Fact]
+        public async void Should_Be_able_to_fetch_existing_key_by_id()
+        {
+            var query = new GetApiKey(_existingEntity.Id);
+            var context = Substitute.For();
+
+            GetApiKeyResult result;
+            using (var uow = CreateUnitOfWork())
+            {
+                var sut = new GetApiKeyHandler(uow);
+                result = await sut.HandleAsync(context, query);
+                uow.SaveChanges();
+            }
+
+            result.Should().NotBeNull();
+            result.GeneratedKey.Should().Be(_existingEntity.GeneratedKey);
+            result.ApplicationName.Should().Be(_existingEntity.ApplicationName);
+            result.AllowedApplications[0].ApplicationId.Should().Be(_application.Id);
+            result.AllowedApplications[0].ApplicationName.Should().Be(_application.Name);
+        }
+
+        private void GetApplication()
+        {
+            if (_application != null)
+                return;
+
+            using (var uow = CreateUnitOfWork())
+            {
+                var repos = new ApplicationRepository(uow);
+                var id = uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications WITH (ReadPast)");
+                if (id is DBNull || id is null)
+                {
+                    _application = new Application(10, "AppTen");
+                    repos.CreateAsync(_application).GetAwaiter().GetResult();
+                }
+                else
+                {
+                    _application = repos.GetByIdAsync((int)id).Result;
+                }
+                uow.SaveChanges();
+            }
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Queries/ListApiKeysHandlerTests.cs b/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Queries/ListApiKeysHandlerTests.cs
new file mode 100644
index 00000000..63e8f36f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Core/ApiKeys/Queries/ListApiKeysHandlerTests.cs
@@ -0,0 +1,59 @@
+using System;
+using Coderr.Server.Api.Core.ApiKeys.Queries;
+using Coderr.Server.App.Core.ApiKeys;
+using Coderr.Server.SqlServer.Core.ApiKeys.Queries;
+using DotNetCqs;
+using FluentAssertions;
+using Griffin.Data.Mapper;
+using NSubstitute;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Coderr.Server.SqlServer.Tests.Core.ApiKeys.Queries
+{
+
+    public class ListApiKeysHandlerTests : IntegrationTest
+    {
+        private readonly ApiKey _existingEntity;
+
+        public ListApiKeysHandlerTests(ITestOutputHelper helper) : base(helper)
+        {
+            _existingEntity = new ApiKey
+            {
+                ApplicationName = "Arne",
+                GeneratedKey = Guid.NewGuid().ToString("N"),
+                SharedSecret = Guid.NewGuid().ToString("N"),
+                CreatedById = 20,
+                CreatedAtUtc = DateTime.UtcNow
+            };
+            _existingEntity.Add(22);
+            using (var uow = CreateUnitOfWork())
+            {
+                uow.Insert(_existingEntity);
+                uow.SaveChanges();
+            }
+
+        }
+
+
+        [Fact]
+        public async void Should_be_able_to_load_a_key()
+        {
+            var query = new ListApiKeys();
+            var context = Substitute.For();
+
+            ListApiKeysResult result;
+            using (var uow = CreateUnitOfWork())
+            {
+                var sut = new ListApiKeysHandler(uow);
+                result = await sut.HandleAsync(context, query);
+                uow.SaveChanges();
+
+            }
+
+            result.Keys.Should().NotBeEmpty();
+            AssertionExtensions.Should((string)result.Keys[0].ApiKey).Be(_existingEntity.GeneratedKey);
+            AssertionExtensions.Should((string)result.Keys[0].ApplicationName).Be(_existingEntity.ApplicationName);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Helpers/DatabaseManager.cs b/src/Server/Coderr.Server.SqlServer.Tests/Helpers/DatabaseManager.cs
new file mode 100644
index 00000000..67d26cbe
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Helpers/DatabaseManager.cs
@@ -0,0 +1,111 @@
+using System;
+using System.Configuration;
+using System.Data;
+using System.Data.SqlClient;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+using Coderr.Server.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Tests.Helpers
+{
+    /// 
+    ///     Purpose of this class is to create and dispose the database.
+    /// 
+    public class DatabaseManager : IDisposable
+    {
+        private const string LocalDbMaster =
+            @"Data Source=(LocalDB)\MSSQLLocalDB;Initial Catalog=master;Integrated Security=True";
+        private const string LocalDbConStr =
+            @"Data Source=(LocalDB)\MSSQLLocalDB;Initial Catalog={DbName};Integrated Security=True;AttachDBFilename={DbFileName};";
+
+        private bool _deleted;
+        private bool _invoked;
+
+        public DatabaseManager(string dbName)
+        {
+            DbName = dbName;
+            ConnectionString = LocalDbConStr;
+            ConnectionString = ConnectionString
+                .Replace("{DbName}", dbName)
+                .Replace("{DbFileName}", GetDbFilename());
+            UpdateToLatestVersion = true;
+        }
+
+        public string ConnectionString { get; }
+        public string DbName { get; }
+
+        public bool UpdateToLatestVersion { get; set; }
+
+        public void Dispose()
+        {
+            if (_deleted)
+                return;
+            _deleted = true;
+        }
+
+        public void CreateDatabase()
+        {
+            var fileName = GetDbFilename();
+            if (File.Exists(fileName))
+            {
+                return;
+            }
+
+            using (var connection = new SqlConnection(LocalDbMaster))
+            {
+                connection.Open();
+                var cmd = connection.CreateCommand();
+                cmd.CommandText = $"CREATE DATABASE {DbName} ON (NAME = N'{DbName}', FILENAME = '{fileName}')";
+                cmd.ExecuteNonQuery();
+            }
+        }
+
+        public AdoNetUnitOfWork CreateUnitOfWork()
+        {
+            return new AdoNetUnitOfWork(OpenConnection(), true);
+        }
+
+        public void InitSchema()
+        {
+            if (_invoked)
+                throw new InvalidOperationException("Already invoked.");
+            _invoked = true;
+            try
+            {
+                var tools = new SqlServerTools(OpenConnection);
+                tools.CreateTables();
+            }
+            catch (SqlException ex)
+            {
+                throw new DataException($"{DbName} [{ConnectionString}] schema init failed.", ex);
+            }
+        }
+
+        public IDbConnection OpenConnection()
+        {
+            return OpenConnection(ConnectionString);
+        }
+        
+        private string GetDbFilename()
+        {
+            var dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Database");
+            if (!Directory.Exists(dir))
+            {
+                Directory.CreateDirectory(dir);
+            }
+
+            return Path.Combine(dir, DbName + ".mdf");
+        }
+        
+
+
+        private IDbConnection OpenConnection(string connectionString)
+        {
+            var connection = new SqlConnection { ConnectionString = connectionString };
+            connection.Open();
+            return connection;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Helpers/TestDataManager.cs b/src/Server/Coderr.Server.SqlServer.Tests/Helpers/TestDataManager.cs
new file mode 100644
index 00000000..9fee51dd
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Helpers/TestDataManager.cs
@@ -0,0 +1,290 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Domain.Core.Account;
+using Coderr.Server.Domain.Core.Applications;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.Domain.Core.User;
+using Coderr.Server.ReportAnalyzer.Incidents;
+using Coderr.Server.SqlServer.Core.Accounts;
+using Coderr.Server.SqlServer.Core.Applications;
+using Coderr.Server.SqlServer.Core.Users;
+using Coderr.Server.SqlServer.ReportAnalyzer;
+using Coderr.Server.SqlServer.Tests.Models;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Tests.Helpers
+{
+    public class TestDataManager
+    {
+        private readonly Func _connectionFactory;
+
+        public TestDataManager(Func connectionFactory)
+        {
+            _connectionFactory = connectionFactory;
+            ConfigStore = new TestConfigStore();
+            TestUser = new TestUser { Email = "demo@fortest.com", Password = "ada123", Username = "DemoTest" };
+        }
+
+        /// 
+        ///     Id of first user created in db
+        /// 
+        /// 
+        ///     
+        ///         The first user is automatically inserted when seeding DB with test data
+        ///     
+        /// 
+        public int AccountId { get; private set; }
+
+        public TestUser TestUser { get; set; }
+
+        public Application Application { get; private set; }
+
+        /// 
+        ///     Id of first application created in db
+        /// 
+        /// 
+        ///     
+        ///         The first application is automatically inserted when seeding DB with test data,  is
+        ///         owner of app.
+        ///     
+        /// 
+        public int ApplicationId { get; private set; }
+
+        public ConfigurationStore ConfigStore { get; private set; }
+
+        public void ActivateAccount(int accountId)
+        {
+            using (var uow = CreateUnitOfWork())
+            {
+                var accountRepository = new AccountRepository(uow);
+                var account = accountRepository.GetByIdAsync(accountId).GetAwaiter().GetResult();
+                account.Activate();
+                accountRepository.UpdateAsync(account).GetAwaiter().GetResult();
+
+                uow.SaveChanges();
+            }
+        }
+
+        /// 
+        ///     Creates an incident and a report.
+        /// 
+        public void CreateReportAndIncident(out int reportId, out int incidentId)
+        {
+            ErrorReportEntity report;
+            using (var uow = CreateUnitOfWork())
+            {
+                report = new ErrorReportEntity(ApplicationId, Guid.NewGuid().ToString("N"), DateTime.UtcNow,
+                        new ErrorReportException(new Exception("mofo")),
+                        new List
+                        {
+                            new ErrorReportContextCollection("Maps", new Dictionary())
+                        })
+                { Title = "Missing here" };
+                report.Init(report.GenerateHashCodeIdentifier());
+
+                uow.SaveChanges();
+            }
+
+            using (var dbContext = CreateUnitOfWork())
+            {
+                var incident = new IncidentBeingAnalyzed(report);
+                var incRepos = new AnalyticsRepository(dbContext);
+                incRepos.CreateIncident(incident);
+                incidentId = incident.Id;
+
+                report.IncidentId = incident.Id;
+                incRepos.CreateReport(report);
+                reportId = report.Id;
+
+                dbContext.SaveChanges();
+            }
+
+
+        }
+
+        /// 
+        ///     Creates an incident and a report.
+        /// 
+        public void CreateReportAndIncident(int applicationId, out int reportId, out int incidentId)
+        {
+            ErrorReportEntity report;
+            using (var uow = CreateUnitOfWork())
+            {
+                report = new ErrorReportEntity(applicationId, Guid.NewGuid().ToString("N"), DateTime.UtcNow,
+                        new ErrorReportException(new Exception("mofo")),
+                        new List
+                        {
+                            new ErrorReportContextCollection("Maps", new Dictionary())
+                        })
+                { Title = "Missing here" };
+                report.Init(report.GenerateHashCodeIdentifier());
+
+                uow.SaveChanges();
+            }
+
+            using (var dbContext = CreateUnitOfWork())
+            {
+                var incident = new IncidentBeingAnalyzed(report);
+                var incRepos = new AnalyticsRepository(dbContext);
+                incRepos.CreateIncident(incident);
+                incidentId = incident.Id;
+
+                report.IncidentId = incident.Id;
+                incRepos.CreateReport(report);
+                reportId = report.Id;
+
+                dbContext.SaveChanges();
+            }
+
+
+        }
+
+        public void CreateUserAndApplication(out int accountId, out int applicationId)
+        {
+            using (var uow = CreateUnitOfWork())
+            {
+                CreateUserAndApplication(uow, out accountId, out applicationId);
+                uow.SaveChanges();
+            }
+        }
+
+        public void CreateUser(TestUser testUser, int applicationId)
+        {
+            using (var uow = CreateUnitOfWork())
+            {
+                var accountRepos = new AccountRepository(uow);
+                var account = new Account(testUser.Username, testUser.Password) { Email = testUser.Email };
+                account.Activate();
+                accountRepos.Create(account);
+                var userRepos = new UserRepository(uow);
+                var user = new User(account.Id, testUser.Username) { EmailAddress = testUser.Email };
+                userRepos.CreateAsync(user).GetAwaiter().GetResult();
+
+                var appRepos = new ApplicationRepository(uow);
+                var member = new ApplicationTeamMember(applicationId, account.Id, "Admin");
+                appRepos.CreateAsync(member).GetAwaiter().GetResult();
+
+                uow.SaveChanges();
+            }
+        }
+
+        private void EnsureServerSettings(string baseUrl)
+        {
+            using (var con = _connectionFactory())
+            {
+                using (var cmd = con.CreateCommand())
+                {
+                    cmd.CommandText = "SELECT Value FROM Settings WHERE Name = 'BaseUrl'";
+                    var value = cmd.ExecuteScalar();
+                    if (value != null)
+                        return;
+                }
+
+                var sql = $@"INSERT INTO Settings (Section, Name, Value) VALUES
+('BaseConfig', 'AllowRegistrations', 'True'), 
+('BaseConfig', 'BaseUrl', '{baseUrl}'), 
+('BaseConfig', 'SenderEmail', 'webtests@coderrapp.com'), 
+('BaseConfig', 'SupportEmail', 'webtests@coderrapp.com'), 
+('ErrorTracking', 'ActivateTracking', 'True'), 
+('ErrorTracking', 'ContactEmail', 'webtests@coderrapp.com'), 
+('ErrorTracking', 'InstallationId', '068e0fc19e90460c86526693488289ee'), 
+('SmtpSettings', 'AccountName', ''), 
+('SmtpSettings', 'AccountPassword', ''), 
+('SmtpSettings', 'SmtpHost', 'localhost'), 
+('SmtpSettings', 'PortNumber', '25'), 
+('SmtpSettings', 'UseSSL', 'False')
+";
+                using (var cmd = con.CreateCommand())
+                {
+                    cmd.CommandText = sql;
+                    cmd.ExecuteNonQuery();
+                }
+            }
+        }
+
+        public Application GetApplication(int id)
+        {
+            using (var uow = CreateUnitOfWork())
+            {
+                var repository = new ApplicationRepository(uow);
+                return repository.GetByIdAsync(id).GetAwaiter().GetResult();
+            }
+        }
+
+        public void ResetDatabase(string baseUrl)
+        {
+            EnsureServerSettings(baseUrl);
+            using (var connection = _connectionFactory())
+            {
+                bool gotData;
+                using (var cmd = connection.CreateCommand())
+                {
+                    cmd.CommandText = "SELECT Id FROM Accounts WHERE Id = 1";
+                    var value = cmd.ExecuteScalar();
+                    gotData = value != null;
+                }
+
+                if (!gotData)
+                {
+                    CreateUserAndApplication(out var accountId, out var applicationId);
+                    AccountId = accountId;
+                    ApplicationId = applicationId;
+                }
+                else
+                {
+                    connection.ExecuteNonQuery("DELETE FROM Accounts WHERE ID > 1");
+                    connection.ExecuteNonQuery("DELETE FROM Applications WHERE ID > 1");
+                    connection.ExecuteNonQuery("DELETE FROM Incidents");
+                    AccountId = 1;
+                    ApplicationId = 1;
+                }
+
+                Application = GetApplication(ApplicationId);
+            }
+        }
+
+        protected void CreateUserAndApplication(IAdoNetUnitOfWork uow, out int accountId, out int applicationId)
+        {
+            var accountRepos = new AccountRepository(uow);
+            var account = new Account(TestUser.Username, TestUser.Password) { Email = TestUser.Email };
+            account.Activate();
+            accountRepos.Create(account);
+            var userRepos = new UserRepository(uow);
+            var user = new User(account.Id, TestUser.Username) { EmailAddress = TestUser.Email };
+            userRepos.CreateAsync(user).GetAwaiter().GetResult();
+
+            var appRepos = new ApplicationRepository(uow);
+            var app = new Application(account.Id, "MyTestApp") { ApplicationType = TypeOfApplication.DesktopApplication };
+            appRepos.CreateAsync(app).GetAwaiter().GetResult();
+            var member = new ApplicationTeamMember(app.Id, account.Id, "Admin");
+            appRepos.CreateAsync(member).GetAwaiter().GetResult();
+
+            accountId = user.AccountId;
+            applicationId = app.Id;
+        }
+
+        private IAdoNetUnitOfWork CreateUnitOfWork()
+        {
+            return new AdoNetUnitOfWork(_connectionFactory(), true);
+        }
+
+        public int CreateApplication(string name, int accountId)
+        {
+            using (var uow = CreateUnitOfWork())
+            {
+                var appRepos = new ApplicationRepository(uow);
+                var app = new Application(accountId, "MyTestApp") { ApplicationType = TypeOfApplication.DesktopApplication };
+                appRepos.CreateAsync(app).GetAwaiter().GetResult();
+                var member = new ApplicationTeamMember(app.Id, accountId, "Admin");
+                appRepos.CreateAsync(member).GetAwaiter().GetResult();
+
+                uow.SaveChanges();
+                return app.Id;
+            }
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/IntegrationTest.cs b/src/Server/Coderr.Server.SqlServer.Tests/IntegrationTest.cs
new file mode 100644
index 00000000..2218d296
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/IntegrationTest.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.IO;
+using System.Reflection;
+using Coderr.Server.Abstractions;
+using Coderr.Server.SqlServer.Core.Accounts;
+using Coderr.Server.SqlServer.Migrations;
+using Coderr.Server.SqlServer.Tests.Helpers;
+using Coderr.Server.SqlServer.Tests.Models;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+using log4net.Config;
+using Xunit;
+using Xunit.Abstractions;
+
+[assembly:
+    TestFramework("Coderr.Server.SqlServer.Tests.Xunit.XunitTestFrameworkWithAssemblyFixture",
+        "Coderr.Server.SqlServer.Tests")]
+
+namespace Coderr.Server.SqlServer.Tests
+{
+    public class IntegrationTest : IDisposable
+    {
+        protected static DatabaseManager _databaseManager;
+        private static readonly object SyncLock = new object();
+        private static bool _inited;
+        public static Action SchemaUpdater;
+
+        protected static List<(string name, string scriptNamespace)> MigrationSources =
+            new List<(string name, string scriptNamespace)>();
+
+        public static AssemblyScanningMappingProvider MappingProvider =
+            (AssemblyScanningMappingProvider)EntityMappingProvider.Provider;
+
+        private readonly TestDataManager _testDataManager;
+
+        static IntegrationTest()
+        {
+            var path2 = AppDomain.CurrentDomain.BaseDirectory;
+            var logRepository = LogManager.GetRepository(Assembly.GetExecutingAssembly());
+            XmlConfigurator.ConfigureAndWatch(logRepository, new FileInfo(Path.Combine(path2, "log4net.config")));
+            var logger = LogManager.GetLogger(typeof(IntegrationTest));
+            logger.Info("Loaded");
+
+
+            AppDomain.CurrentDomain.DomainUnload += (o, e) =>
+            {
+                if (_databaseManager == null)
+                    return;
+                _databaseManager.Dispose();
+                _databaseManager = null;
+            };
+
+            EntityMappingProvider.Provider = new AssemblyScanningMappingProvider();
+            MappingProvider.Scan(typeof(AccountRepository).Assembly);
+        }
+
+        public IntegrationTest(ITestOutputHelper output)
+        {
+            lock (SyncLock)
+            {
+                if (!_inited)
+                {
+                    foreach (var source in MigrationSources)
+                        SqlServerTools.AddMigrationRunner(new MigrationRunner(OpenConnection, source.name,
+                            source.scriptNamespace));
+
+                    _databaseManager = new DatabaseManager("CoderrTest");
+                    _databaseManager.CreateDatabase();
+                    _databaseManager.InitSchema();
+                    if (SchemaUpdater != null)
+                        using (var connection = _databaseManager.OpenConnection())
+                        {
+                            SchemaUpdater(connection);
+                        }
+
+                    _inited = true;
+                }
+            }
+
+            _testDataManager = new TestDataManager(_databaseManager.OpenConnection)
+            {
+                TestUser = new TestUser {Email = "test@somewhere.com", Password = "123456", Username = "admin"}
+            };
+        }
+
+        public int FirstApplicationId => _testDataManager.ApplicationId;
+        public int FirstUserId => _testDataManager.AccountId;
+
+        public void Dispose()
+        {
+            Dispose(true);
+            _databaseManager.Dispose();
+        }
+
+
+        protected int CreateApplication(string applicationName, int accountIdForAdmin)
+        {
+            return _testDataManager.CreateApplication(applicationName, accountIdForAdmin);
+        }
+
+        protected void CreateReportAndIncident(out int reportId, out int incidentId)
+        {
+            _testDataManager.CreateReportAndIncident(out reportId, out incidentId);
+        }
+
+        protected void CreateReportAndIncident(int applicationId, out int reportId, out int incidentId)
+        {
+            _testDataManager.CreateReportAndIncident(applicationId, out reportId, out incidentId);
+        }
+
+        protected IAdoNetUnitOfWork CreateUnitOfWork()
+        {
+            return _databaseManager.CreateUnitOfWork();
+        }
+
+
+        protected virtual void Dispose(bool isBeingDisposed)
+        {
+        }
+
+        protected IDbConnection OpenConnection()
+        {
+            return _databaseManager.OpenConnection();
+        }
+
+        protected void ResetDatabase(string baseUrl = "http://localhost:53844")
+        {
+            _testDataManager.ResetDatabase(baseUrl);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Models/TestUser.cs b/src/Server/Coderr.Server.SqlServer.Tests/Models/TestUser.cs
new file mode 100644
index 00000000..6db57da9
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Models/TestUser.cs
@@ -0,0 +1,9 @@
+namespace Coderr.Server.SqlServer.Tests.Models
+{
+    public class TestUser
+    {
+        public string Username { get; set; }
+        public string Password { get; set; }
+        public string Email { get; set; }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Modules/Geolocation/ErrorOriginRepositoryTests.cs b/src/Server/Coderr.Server.SqlServer.Tests/Modules/Geolocation/ErrorOriginRepositoryTests.cs
new file mode 100644
index 00000000..6422d2f7
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Modules/Geolocation/ErrorOriginRepositoryTests.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Modules.ErrorOrigins;
+using Coderr.Server.SqlServer.Modules.Geolocation;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Coderr.Server.SqlServer.Tests.Modules.Geolocation
+{
+    public class ErrorOriginRepositoryTests : IntegrationTest
+    {
+        private int _reportId;
+        private int _incidentId;
+
+        public ErrorOriginRepositoryTests(ITestOutputHelper helper) : base(helper)
+        {
+            ResetDatabase();
+            CreateReportAndIncident(out _reportId, out _incidentId);
+        }
+
+        [Fact]
+        public async Task Can_store_origin()
+        {
+            var origin = new ErrorOrigin("127.0.0.1", 934.934, 28.282);
+            using (var uow = CreateUnitOfWork())
+            {
+                var handler = new ErrorOriginRepository(uow);
+                await handler.CreateAsync(origin, FirstApplicationId, _incidentId, _reportId);
+                uow.SaveChanges();
+            }
+        }
+        
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/TestConfigStore.cs b/src/Server/Coderr.Server.SqlServer.Tests/TestConfigStore.cs
new file mode 100644
index 00000000..0367f08c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/TestConfigStore.cs
@@ -0,0 +1,17 @@
+using Coderr.Server.Abstractions.Config;
+
+namespace Coderr.Server.SqlServer.Tests
+{
+    public class TestConfigStore : ConfigurationStore
+    {
+        public override T Load()
+        {
+            return new T();
+        }
+
+        public override void Store(IConfigurationSection section)
+        {
+            
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Xunit/MethodLogger.cs b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/MethodLogger.cs
new file mode 100644
index 00000000..f2a063d0
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/MethodLogger.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using log4net;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Coderr.Server.SqlServer.Tests.Xunit
+{
+    public class MethodLogger: XunitTestClassRunner
+    {
+        private ILog _logger = LogManager.GetLogger(typeof(MethodLogger));
+
+        public MethodLogger(ITestClass testClass, IReflectionTypeInfo @class, IEnumerable testCases, IMessageSink diagnosticMessageSink, IMessageBus messageBus, ITestCaseOrderer testCaseOrderer, ExceptionAggregator aggregator, CancellationTokenSource cancellationTokenSource, IDictionary collectionFixtureMappings) : base(testClass, @class, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator, cancellationTokenSource, collectionFixtureMappings)
+        {
+        }
+
+        protected override async Task RunTestMethodAsync(ITestMethod testMethod, IReflectionMethodInfo method, IEnumerable testCases,
+            object[] constructorArguments)
+        {
+            try
+            {
+                _logger.Info($"Running {testMethod.TestClass.Class.Name}.{testMethod.Method.Name}");
+
+                var t = base.RunTestMethodAsync(testMethod, method, testCases, constructorArguments);
+                var delay = Task.Delay(5000);
+                await Task.WhenAny(t, delay);
+                if (!t.IsCompleted)
+                {
+                    throw new TimeoutException($"Timeout: {testMethod.TestClass.Class.Name}.{testMethod.Method.Name}");
+                }
+
+                //var result= await base.RunTestMethodAsync(testMethod, method, testCases, constructorArguments);
+                _logger.Info(".. completed " + testMethod.TestClass.Class.Name + "." + testMethod.Method.Name);
+                return t.Result;
+            }
+            catch (Exception e)
+            {
+                _logger.Info(".. failed " + testMethod.TestClass.Class.Name + "." + testMethod.Method.Name, e);
+                throw;
+            }
+            
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestAssemblyRunnerWithAssemblyFixture.cs b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestAssemblyRunnerWithAssemblyFixture.cs
new file mode 100644
index 00000000..6aa96cda
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestAssemblyRunnerWithAssemblyFixture.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Coderr.Server.SqlServer.Tests.Xunit
+{
+    public class XunitTestAssemblyRunnerWithAssemblyFixture : XunitTestAssemblyRunner
+    {
+        readonly Dictionary assemblyFixtureMappings = new Dictionary();
+
+        public XunitTestAssemblyRunnerWithAssemblyFixture(ITestAssembly testAssembly,
+                                                          IEnumerable testCases,
+                                                          IMessageSink diagnosticMessageSink,
+                                                          IMessageSink executionMessageSink,
+                                                          ITestFrameworkExecutionOptions executionOptions)
+            : base(testAssembly, testCases, diagnosticMessageSink, executionMessageSink, executionOptions)
+        { }
+
+        protected override async Task AfterTestAssemblyStartingAsync()
+        {
+            // Let everything initialize
+            await base.AfterTestAssemblyStartingAsync();
+        }
+
+        protected override Task BeforeTestAssemblyFinishedAsync()
+        {
+            // Make sure we clean up everybody who is disposable, and use Aggregator.Run to isolate Dispose failures
+            foreach (var disposable in assemblyFixtureMappings.Values.OfType())
+                Aggregator.Run(disposable.Dispose);
+
+            return base.BeforeTestAssemblyFinishedAsync();
+        }
+
+        protected override Task RunTestCollectionAsync(IMessageBus messageBus,
+                                                                   ITestCollection testCollection,
+                                                                   IEnumerable testCases,
+                                                                   CancellationTokenSource cancellationTokenSource)
+
+
+            => new XunitTestCollectionRunnerWithAssemblyFixture(assemblyFixtureMappings, testCollection, testCases, DiagnosticMessageSink, messageBus, TestCaseOrderer, new ExceptionAggregator(Aggregator), cancellationTokenSource).RunAsync();
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestCollectionRunnerWithAssemblyFixture.cs b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestCollectionRunnerWithAssemblyFixture.cs
new file mode 100644
index 00000000..a7b6e186
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestCollectionRunnerWithAssemblyFixture.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Coderr.Server.SqlServer.Tests.Xunit
+{
+    public class XunitTestCollectionRunnerWithAssemblyFixture : XunitTestCollectionRunner
+    {
+        private readonly Dictionary _assemblyFixtureMappings;
+        private readonly IMessageSink _diagnosticMessageSink;
+
+        public XunitTestCollectionRunnerWithAssemblyFixture(Dictionary assemblyFixtureMappings,
+            ITestCollection testCollection,
+            IEnumerable testCases,
+            IMessageSink diagnosticMessageSink,
+            IMessageBus messageBus,
+            ITestCaseOrderer testCaseOrderer,
+            ExceptionAggregator aggregator,
+            CancellationTokenSource cancellationTokenSource)
+            : base(testCollection, testCases, diagnosticMessageSink, messageBus, testCaseOrderer, aggregator,
+                cancellationTokenSource)
+        {
+            _assemblyFixtureMappings = assemblyFixtureMappings;
+            _diagnosticMessageSink = diagnosticMessageSink;
+        }
+
+        protected override Task RunTestClassAsync(ITestClass testClass, IReflectionTypeInfo @class,
+            IEnumerable testCases)
+        {
+            // Don't want to use .Concat + .ToDictionary because of the possibility of overriding types,
+            // so instead we'll just let collection fixtures override assembly fixtures.
+            var combinedFixtures = new Dictionary(_assemblyFixtureMappings);
+            foreach (var kvp in CollectionFixtureMappings)
+                combinedFixtures[kvp.Key] = kvp.Value;
+
+            // We've done everything we need, so let the built-in types do the rest of the heavy lifting
+            return new MethodLogger(testClass, @class, testCases, _diagnosticMessageSink, MessageBus, TestCaseOrderer,
+                new ExceptionAggregator(Aggregator), CancellationTokenSource, combinedFixtures).RunAsync();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestFrameworkExecutorWithAssemblyFixture.cs b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestFrameworkExecutorWithAssemblyFixture.cs
new file mode 100644
index 00000000..ccb383df
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestFrameworkExecutorWithAssemblyFixture.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using System.Reflection;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Coderr.Server.SqlServer.Tests.Xunit
+{
+    public class XunitTestFrameworkExecutorWithAssemblyFixture : XunitTestFrameworkExecutor
+    {
+        public XunitTestFrameworkExecutorWithAssemblyFixture(AssemblyName assemblyName, ISourceInformationProvider sourceInformationProvider, IMessageSink diagnosticMessageSink)
+            : base(assemblyName, sourceInformationProvider, diagnosticMessageSink)
+        { }
+
+        protected override async void RunTestCases(IEnumerable testCases, IMessageSink executionMessageSink, ITestFrameworkExecutionOptions executionOptions)
+        {
+            using (var assemblyRunner = new XunitTestAssemblyRunnerWithAssemblyFixture(TestAssembly, testCases, DiagnosticMessageSink, executionMessageSink, executionOptions))
+                await assemblyRunner.RunAsync();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestFrameworkWithAssemblyFixture.cs b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestFrameworkWithAssemblyFixture.cs
new file mode 100644
index 00000000..0934b0ca
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/Xunit/XunitTestFrameworkWithAssemblyFixture.cs
@@ -0,0 +1,16 @@
+using System.Reflection;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace Coderr.Server.SqlServer.Tests.Xunit
+{
+    public class XunitTestFrameworkWithAssemblyFixture : XunitTestFramework
+    {
+        public XunitTestFrameworkWithAssemblyFixture(IMessageSink messageSink)
+            : base(messageSink)
+        { }
+
+        protected override ITestFrameworkExecutor CreateExecutor(AssemblyName assemblyName)
+            => new XunitTestFrameworkExecutorWithAssemblyFixture(assemblyName, SourceInformationProvider, DiagnosticMessageSink);
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/app.config b/src/Server/Coderr.Server.SqlServer.Tests/app.config
new file mode 100644
index 00000000..a5929ca5
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/app.config
@@ -0,0 +1,8 @@
+
+
+
+  
+    
+  
+
+
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/codeRR.Server.SqlServer.Tests.v3.ncrunchproject b/src/Server/Coderr.Server.SqlServer.Tests/codeRR.Server.SqlServer.Tests.v3.ncrunchproject
new file mode 100644
index 00000000..319cd523
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/codeRR.Server.SqlServer.Tests.v3.ncrunchproject
@@ -0,0 +1,5 @@
+
+  
+    True
+  
+
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer.Tests/log4net.config b/src/Server/Coderr.Server.SqlServer.Tests/log4net.config
new file mode 100644
index 00000000..edb55826
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer.Tests/log4net.config
@@ -0,0 +1,20 @@
+
+
+
+
+  
+    
+    
+    
+    
+    
+    
+      
+    
+  
+
+  
+    
+    
+  
+
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Coderr - Backup.Server.SqlServer.csproj b/src/Server/Coderr.Server.SqlServer/Coderr - Backup.Server.SqlServer.csproj
new file mode 100644
index 00000000..cb229780
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Coderr - Backup.Server.SqlServer.csproj	
@@ -0,0 +1,43 @@
+
+  
+    netstandard2.0
+    Coderr.Server.SqlServer
+    Coderr.Server.SqlServer
+    Debug;Release;Premise
+  
+  
+    
+    
+    
+    
+    
+    
+  
+
+  
+    
+    
+    
+    
+    
+    
+    
+  
+
+  
+    
+  
+
+  
+    
+    
+    
+    
+    
+    
+  
+
+  
+    
+  
+
diff --git a/src/Server/Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj b/src/Server/Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj
new file mode 100644
index 00000000..b0823a49
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj
@@ -0,0 +1,39 @@
+
+  
+    netstandard2.0
+    Coderr.Server.SqlServer
+    Coderr.Server.SqlServer
+    Debug;Release;Premise
+  
+  
+    
+    
+    
+    
+    
+    
+  
+
+  
+    
+    
+    
+    
+    
+    
+    
+  
+
+  
+    
+  
+
+  
+    
+    
+    
+    
+    
+    
+  
+
diff --git a/src/Server/OneTrueError.SqlServer/Core/Accounts/AccountMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Accounts/AccountMapper.cs
similarity index 85%
rename from src/Server/OneTrueError.SqlServer/Core/Accounts/AccountMapper.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Accounts/AccountMapper.cs
index 309657e2..be311875 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Accounts/AccountMapper.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Accounts/AccountMapper.cs
@@ -1,25 +1,25 @@
-using System;
-using Griffin.Data.Mapper;
-using OneTrueError.App.Core.Accounts;
-
-namespace OneTrueError.SqlServer.Core.Accounts
-{
-    public class AccountMapper : CrudEntityMapper
-    {
-        public AccountMapper() : base("Accounts")
-        {
-            Property(x => x.Id)
-                .PrimaryKey(true);
-
-            Property(x => x.AccountState)
-                .ToPropertyValue(o => (AccountState) Enum.Parse(typeof(AccountState), (string) o, true))
-                .ToColumnValue(o => o.ToString());
-
-            Property(x => x.UpdatedAtUtc)
-                .ToColumnValue(DbConverters.ToSqlNull);
-
-            Property(x => x.LastLoginAtUtc)
-                .ToColumnValue(DbConverters.ToSqlNull);
-        }
-    }
+using System;
+using Coderr.Server.Domain.Core.Account;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Accounts
+{
+    public class AccountMapper : CrudEntityMapper
+    {
+        public AccountMapper() : base("Accounts")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+
+            Property(x => x.AccountState)
+                .ToPropertyValue(o => (AccountState) Enum.Parse(typeof(AccountState), (string) o, true))
+                .ToColumnValue(o => o.ToString());
+
+            Property(x => x.UpdatedAtUtc)
+                .ToColumnValue(DbConverters.ToSqlNull);
+
+            Property(x => x.LastLoginAtUtc)
+                .ToColumnValue(DbConverters.ToSqlNull);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Accounts/AccountRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Accounts/AccountRepository.cs
new file mode 100644
index 00000000..4611842a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Accounts/AccountRepository.cs
@@ -0,0 +1,230 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.Account;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Core.Accounts
+{
+    [ContainerService]
+    public class AccountRepository : IAccountRepository
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+        private ILog _logger = LogManager.GetLogger(typeof(AccountRepository));
+
+        public AccountRepository(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow ?? throw new ArgumentNullException(nameof(uow));
+            LogManager.GetLogger(typeof(AccountRepository)).Info("UOW hash: " + _uow.GetHashCode());
+        }
+
+        /// 
+        public Task CountAsync()
+        {
+            return Task.FromResult((int)_uow.ExecuteScalar("SELECT CAST(count(*) as int) FROM Accounts"));
+        }
+
+
+        /// 
+        public async Task CreateAsync(Account account)
+        {
+            await _uow.InsertAsync(account);
+        }
+
+        /// 
+        public async Task FindByActivationKeyAsync(string activationKey)
+        {
+            using (var cmd = _uow.CreateCommand())
+            {
+                cmd.CommandText = "SELECT * FROM Accounts WHERE ActivationKey=@key";
+                cmd.AddParameter("key", activationKey);
+                var accounts= await cmd.ToListAsync(new AccountMapper());
+                if (accounts.Count == 0)
+                    return null;
+
+                _logger.Error($"Found {accounts.Count} accounts, expected one.");
+                return accounts.First();
+            }
+        }
+
+        /// 
+        public async Task UpdateAsync(Account account)
+        {
+            using (var cmd = (DbCommand) _uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    "UPDATE Accounts SET " +
+                    " Username = @Username, " +
+                    " HashedPassword = @HashedPassword, " +
+                    " Salt = @Salt, " +
+                    " CreatedAtUtc = @CreatedAtUtc, " +
+                    " AccountState = @AccountState, " +
+                    " Email = @Email, " +
+                    " UpdatedAtUtc = @UpdatedAtUtc, " +
+                    " ActivationKey = @ActivationKey, " +
+                    " LoginAttempts = @LoginAttempts, " +
+                    " LastLoginAtUtc = @LastLoginAtUtc " +
+                    "WHERE Id = @Id";
+                cmd.AddParameter("@Id", account.Id);
+                cmd.AddParameter("@Username", account.UserName);
+                cmd.AddParameter("@HashedPassword", account.HashedPassword);
+                cmd.AddParameter("@Salt", account.Salt);
+                cmd.AddParameter("@CreatedAtUtc", account.CreatedAtUtc);
+                cmd.AddParameter("@AccountState", account.AccountState.ToString());
+                cmd.AddParameter("@Email", account.Email);
+                cmd.AddParameter("@UpdatedAtUtc",
+                    account.UpdatedAtUtc == DateTime.MinValue ? (object) null : account.UpdatedAtUtc);
+                cmd.AddParameter("@ActivationKey", account.ActivationKey);
+                cmd.AddParameter("@LoginAttempts", account.LoginAttempts);
+                cmd.AddParameter("@LastLoginAtUtc",
+                    account.LastLoginAtUtc == DateTime.MinValue ? (object) null : account.LastLoginAtUtc);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        /// 
+        public async Task FindByUserNameAsync(string userName)
+        {
+            if (userName == null) throw new ArgumentNullException(nameof(userName));
+
+            using (var cmd = (DbCommand) _uow.CreateCommand())
+            {
+                cmd.CommandText = "SELECT TOP 1 * FROM Accounts WHERE UserName=@uname";
+                cmd.AddParameter("uname", userName);
+                return await cmd.FirstOrDefaultAsync(new AccountMapper());
+            }
+        }
+
+        public async Task GetByIdAsync(int id)
+        {
+            if (id <= 0) throw new ArgumentNullException(nameof(id));
+
+            using (var cmd = _uow.CreateCommand())
+            {
+                cmd.CommandText = "SELECT * FROM Accounts WHERE Id=@id";
+                cmd.AddParameter("id", id);
+                return await cmd.FirstAsync();
+            }
+        }
+
+        /// 
+        public async Task FindByEmailAsync(string emailAddress)
+        {
+            if (emailAddress == null) throw new ArgumentNullException(nameof(emailAddress));
+
+            using (var cmd = _uow.CreateCommand())
+            {
+                cmd.CommandText = "SELECT * FROM Accounts WHERE Email=@email";
+                cmd.AddParameter("email", emailAddress);
+                return await cmd.FirstOrDefaultAsync(new AccountMapper());
+            }
+        }
+
+        /// 
+        public async Task> GetByIdAsync(int[] ids)
+        {
+            if (ids == null) throw new ArgumentNullException(nameof(ids));
+
+            using (var cmd = (DbCommand) _uow.CreateCommand())
+            {
+                var idStr = string.Join(",", ids.Select(x => "'" + x + "'"));
+                cmd.CommandText = $"SELECT * FROM Accounts WHERE Id IN ({idStr})";
+                return await cmd.ToListAsync();
+            }
+        }
+
+
+        /// 
+        public async Task IsEmailAddressTakenAsync(string email)
+        {
+            if (email == null) throw new ArgumentNullException(nameof(email));
+
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT TOP 1 Email FROM Accounts WHERE Email = @Email";
+                cmd.AddParameter("Email", email);
+                var result = await cmd.ExecuteScalarAsync();
+                return result != null && result != DBNull.Value;
+            }
+        }
+
+
+        /// 
+        public async Task IsUserNameTakenAsync(string userName)
+        {
+            if (userName == null) throw new ArgumentNullException(nameof(userName));
+
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT TOP 1 UserName FROM Accounts WHERE UserName = @userName";
+                cmd.AddParameter("userName", userName);
+                var result = await cmd.ExecuteScalarAsync();
+                return result != null && result != DBNull.Value;
+            }
+        }
+
+        public void Create(Account account)
+        {
+            if (account == null) throw new ArgumentNullException(nameof(account));
+            using (var cmd = _uow.CreateCommand())
+            {
+                //for systems where ID must be specified
+                if (account.Id > 0)
+                {
+                    cmd.CommandText =
+                        "INSERT INTO Accounts (Id, Username, HashedPassword, Salt, CreatedAtUtc, AccountState, Email, UpdatedAtUtc, ActivationKey, LoginAttempts, LastLoginAtUtc) " +
+                        " VALUES(@Id, @Username, @HashedPassword, @Salt, @CreatedAtUtc, @AccountState, @Email, @UpdatedAtUtc, @ActivationKey, @LoginAttempts, @LastLoginAtUtc); SELECT CAST(SCOPE_IDENTITY() AS INT);";
+                    cmd.AddParameter("id", account.Id);
+
+                }
+                else
+                {
+                    cmd.CommandText =
+                        "INSERT INTO Accounts (Username, HashedPassword, Salt, CreatedAtUtc, AccountState, Email, UpdatedAtUtc, ActivationKey, LoginAttempts, LastLoginAtUtc) " +
+                        " VALUES(@Username, @HashedPassword, @Salt, @CreatedAtUtc, @AccountState, @Email, @UpdatedAtUtc, @ActivationKey, @LoginAttempts, @LastLoginAtUtc); SELECT CAST(SCOPE_IDENTITY() AS INT);";
+                }
+                cmd.AddParameter("@Username", account.UserName);
+                cmd.AddParameter("@HashedPassword", account.HashedPassword);
+                cmd.AddParameter("@Salt", account.Salt);
+                cmd.AddParameter("@CreatedAtUtc", account.CreatedAtUtc);
+                cmd.AddParameter("@AccountState", account.AccountState.ToString());
+                cmd.AddParameter("@Email", account.Email);
+                cmd.AddParameter("@UpdatedAtUtc",
+                    account.UpdatedAtUtc == DateTime.MinValue ? (object) null : account.UpdatedAtUtc);
+                cmd.AddParameter("@ActivationKey", account.ActivationKey);
+                cmd.AddParameter("@LoginAttempts", account.LoginAttempts);
+                cmd.AddParameter("@LastLoginAtUtc",
+                    account.LastLoginAtUtc == DateTime.MinValue ? (object) null : account.LastLoginAtUtc);
+
+                if (account.Id > 0)
+                {
+                    cmd.ExecuteNonQuery();
+                }
+                else
+                {
+                    var value = (int) cmd.ExecuteScalar();
+                    account.GetType().GetProperty("Id").SetValue(account, value);
+                }
+            }
+        }
+
+        public async Task GetByUserNameAsync(string userName)
+        {
+            if (userName == null) throw new ArgumentNullException(nameof(userName));
+
+            using (var cmd = _uow.CreateCommand())
+            {
+                cmd.CommandText = "SELECT * FROM Accounts WHERE UserName=@userName";
+                cmd.AddParameter("userName", userName);
+                return await cmd.FirstAsync(new AccountMapper());
+            }
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs
new file mode 100644
index 00000000..8fa8d6e4
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Accounts.Queries;
+using Coderr.Server.Domain.Core.Account;
+using DotNetCqs;
+
+namespace Coderr.Server.SqlServer.Core.Accounts.QueryHandlers
+{
+    public class GetAccountEmailByIdHandler : IQueryHandler
+    {
+        private readonly IAccountRepository _accountRepository;
+
+        public GetAccountEmailByIdHandler(IAccountRepository accountRepository)
+        {
+            if (accountRepository == null) throw new ArgumentNullException(nameof(accountRepository));
+            _accountRepository = accountRepository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetAccountEmailById query)
+        {
+            var usr = await _accountRepository.GetByIdAsync((int) query.AccountId);
+            return usr.Email;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/ListAccountsHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/ListAccountsHandler.cs
new file mode 100644
index 00000000..5790cd45
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Accounts/QueryHandlers/ListAccountsHandler.cs
@@ -0,0 +1,26 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Accounts.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Accounts.QueryHandlers
+{
+    public class ListAccountsHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private static readonly IEntityMapper _mapper = new MirrorMapper();
+
+        public ListAccountsHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, ListAccounts query)
+        {
+            var sql = "SELECT Id AccountId, UserName, CreatedAtUtc, Email FROM Accounts ORDER BY UserName";
+            var users = await _unitOfWork.ToListAsync(_mapper, sql);
+            return new ListAccountsResult() { Accounts = users.ToArray() };
+        }
+    }
+}
diff --git a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/ApiKeyRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/ApiKeyRepository.cs
similarity index 87%
rename from src/Server/OneTrueError.SqlServer/Core/ApiKeys/ApiKeyRepository.cs
rename to src/Server/Coderr.Server.SqlServer/Core/ApiKeys/ApiKeyRepository.cs
index 713fa68b..fa3914e9 100644
--- a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/ApiKeyRepository.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/ApiKeyRepository.cs
@@ -1,181 +1,194 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Griffin.Container;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.App.Core.ApiKeys;
-using OneTrueError.Infrastructure.Security;
-using OneTrueError.SqlServer.Core.ApiKeys.Mappings;
-
-namespace OneTrueError.SqlServer.Core.ApiKeys
-{
-    /// 
-    ///     SQL Server implementation of .
-    /// 
-    [Component]
-    public class ApiKeyRepository : IApiKeyRepository
-    {
-        private readonly IAdoNetUnitOfWork _uow;
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// Active unit of work
-        public ApiKeyRepository(IAdoNetUnitOfWork uow)
-        {
-            if (uow == null) throw new ArgumentNullException(nameof(uow));
-
-            _uow = uow;
-        }
-
-        /// 
-        ///     Delete all mappings that are for a specific application
-        /// 
-        /// id for the ApiKey that the application is associated with
-        /// Application to remove mapping for
-        /// 
-        public Task DeleteApplicationMappingAsync(int apiKeyId, int applicationId)
-        {
-            _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId AND ApplicationId = @appId",
-                new {appId = applicationId, keyId = apiKeyId});
-            return Task.FromResult(null);
-        }
-
-        /// 
-        ///     Delete a specific ApiKey.
-        /// 
-        /// 
-        /// 
-        public Task DeleteAsync(int keyId)
-        {
-            _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId", new {keyId});
-            _uow.ExecuteNonQuery("DELETE FROM [ApiKeys] WHERE Id = @keyId", new {keyId});
-            return Task.FromResult(null);
-        }
-
-        /// 
-        ///     Get an key by using the generated string.
-        /// 
-        /// key
-        /// key
-        /// Given key was not found.
-        public async Task GetByKeyAsync(string apiKey)
-        {
-            if (apiKey == null) throw new ArgumentNullException(nameof(apiKey));
-
-            var key = await _uow.FirstAsync("GeneratedKey=@1", apiKey);
-            var sql = "SELECT [ApplicationId] FROM [ApiKeyApplications] WHERE [ApiKeyId] = @1";
-            var apps = await _uow.ToListAsync(new IntMapper(), sql, key.Id);
-            foreach (var app in apps)
-            {
-                key.Add(app);
-            }
-            return key;
-        }
-
-        /// 
-        ///     Get all ApiKeys that maps to a specific application
-        /// 
-        /// application id
-        /// list
-        public async Task> GetForApplicationAsync(int applicationId)
-        {
-            var apiKeyIds = new List();
-            using (var cmd = _uow.CreateDbCommand())
-            {
-                cmd.CommandText = "SELECT ApiKeyId FROM ApiKeyApplications WHERE ApplicationId = @id";
-                cmd.AddParameter("id", applicationId);
-                using (var reader = await cmd.ExecuteReaderAsync())
-                {
-                    while (await reader.ReadAsync())
-                    {
-                        apiKeyIds.Add(reader.GetInt32(0));
-                    }
-                }
-            }
-
-            var keys = new List();
-            foreach (var id in apiKeyIds)
-            {
-                var key = await GetByKeyIdAsync(id);
-                keys.Add(key);
-            }
-            return keys;
-        }
-
-        /// 
-        ///     Create a new key
-        /// 
-        /// key to create
-        /// task
-        public async Task CreateAsync(ApiKey key)
-        {
-            if (key == null) throw new ArgumentNullException(nameof(key));
-
-            await _uow.InsertAsync(key);
-            foreach (var claim in key.Claims.Where(x => x.Type == OneTrueClaims.Application))
-            {
-                AddApplication(key.Id, int.Parse(claim.Value));
-            }
-        }
-
-        /// 
-        ///     Get an key by using the generated string.
-        /// 
-        /// PK
-        /// key
-        /// Given key was not found.
-        public async Task GetByKeyIdAsync(int id)
-        {
-            var key = await _uow.FirstAsync("id=@1", id);
-            var sql = "SELECT [ApplicationId] FROM [ApiKeyApplications] WHERE [ApiKeyId] = @1";
-            var apps = await _uow.ToListAsync(new IntMapper(), sql, key.Id);
-            foreach (var app in apps)
-            {
-                key.Add(app);
-            }
-            return key;
-        }
-
-        /// 
-        ///     Update an existing key
-        /// 
-        /// key
-        /// task
-        public async Task UpdateAsync(ApiKey key)
-        {
-            if (key == null) throw new ArgumentNullException(nameof(key));
-
-            await _uow.InsertAsync(key);
-
-            var existingMappings =
-                await _uow.ToListAsync("SELECT ApplicationId FROM ApiKeyApplications WHERE ApiKeyId=@1",
-                    key);
-
-            var apps = key.Claims.Select(x => int.Parse(x.Value));
-            var removed = existingMappings.Except(apps);
-            foreach (var applicationId in removed)
-            {
-                _uow.Execute("DELETE FROM ApiKeyApplications WHERE ApiKeyId = @1 AND ApplicationId = @2",
-                    new[] {key.Id, applicationId});
-            }
-
-            var added = apps.Except(existingMappings);
-            foreach (var id in added)
-            {
-                AddApplication(key.Id, id);
-            }
-        }
-
-        private void AddApplication(int apiKeyId, int applicationId)
-        {
-            _uow.Execute("INSERT INTO [ApiKeyApplications] (ApiKeyId, ApplicationId) VALUES(@api, @app)", new
-            {
-                api = apiKeyId,
-                app = applicationId
-            });
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.App.Core.ApiKeys;
+using Coderr.Server.SqlServer.Core.ApiKeys.Mappings;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.ApiKeys
+{
+    /// 
+    ///     SQL Server implementation of .
+    /// 
+    [ContainerService]
+    public class ApiKeyRepository : IApiKeyRepository
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// Active unit of work
+        public ApiKeyRepository(IAdoNetUnitOfWork uow)
+        {
+            if (uow == null) throw new ArgumentNullException(nameof(uow));
+
+            _uow = uow;
+        }
+
+        /// 
+        ///     Delete all mappings that are for a specific application
+        /// 
+        /// id for the ApiKey that the application is associated with
+        /// Application to remove mapping for
+        /// 
+        public Task DeleteApplicationMappingAsync(int apiKeyId, int applicationId)
+        {
+            _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId AND ApplicationId = @appId",
+                new { appId = applicationId, keyId = apiKeyId });
+            return Task.FromResult(null);
+        }
+
+        /// 
+        ///     Delete a specific ApiKey.
+        /// 
+        /// 
+        /// 
+        public Task DeleteAsync(int keyId)
+        {
+            _uow.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @keyId", new { keyId });
+            _uow.ExecuteNonQuery("DELETE FROM [ApiKeys] WHERE Id = @keyId", new { keyId });
+            return Task.FromResult(null);
+        }
+
+        /// 
+        ///     Get an key by using the generated string.
+        /// 
+        /// key
+        /// key
+        /// Given key was not found.
+        public async Task GetByKeyAsync(string apiKey)
+        {
+            if (apiKey == null) throw new ArgumentNullException(nameof(apiKey));
+
+            var key = await _uow.FirstAsync("GeneratedKey=@1", apiKey);
+            var sql = "SELECT [ApplicationId] FROM [ApiKeyApplications] WHERE [ApiKeyId] = @1";
+            var apps = await _uow.ToListAsync(new IntMapper(), sql, key.Id);
+
+            // apikeys without application restrictions should have access to all applications.
+            if (apps.Count == 0)
+            {
+                sql = "SELECT [Id] FROM [Applications]";
+                apps = await _uow.ToListAsync(new IntMapper(), sql);
+
+            }
+
+            foreach (var app in apps)
+            {
+                key.Add(app);
+            }
+
+
+            return key;
+        }
+
+        /// 
+        ///     Get all ApiKeys that maps to a specific application
+        /// 
+        /// application id
+        /// list
+        public async Task> GetForApplicationAsync(int applicationId)
+        {
+            var apiKeyIds = new List();
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT ApiKeyId FROM ApiKeyApplications WHERE ApplicationId = @id";
+                cmd.AddParameter("id", applicationId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        apiKeyIds.Add(reader.GetInt32(0));
+                    }
+                }
+            }
+
+            var keys = new List();
+            foreach (var id in apiKeyIds)
+            {
+                var key = await GetByKeyIdAsync(id);
+                keys.Add(key);
+            }
+            return keys;
+        }
+
+        /// 
+        ///     Create a new key
+        /// 
+        /// key to create
+        /// task
+        public async Task CreateAsync(ApiKey key)
+        {
+            if (key == null) throw new ArgumentNullException(nameof(key));
+
+            await _uow.InsertAsync(key);
+            foreach (var claim in key.Claims.Where(x => x.Type == CoderrClaims.Application))
+            {
+                AddApplication(key.Id, int.Parse(claim.Value));
+            }
+        }
+
+        /// 
+        ///     Get an key by using the generated string.
+        /// 
+        /// PK
+        /// key
+        /// Given key was not found.
+        public async Task GetByKeyIdAsync(int id)
+        {
+            var key = await _uow.FirstAsync("id=@1", id);
+            var sql = "SELECT [ApplicationId] FROM [ApiKeyApplications] WHERE [ApiKeyId] = @1";
+            var apps = await _uow.ToListAsync(new IntMapper(), sql, key.Id);
+            foreach (var app in apps)
+            {
+                key.Add(app);
+            }
+            return key;
+        }
+
+        /// 
+        ///     Update an existing key
+        /// 
+        /// key
+        /// task
+        public async Task UpdateAsync(ApiKey key)
+        {
+            if (key == null) throw new ArgumentNullException(nameof(key));
+
+            await _uow.InsertAsync(key);
+
+            var existingMappings =
+                await _uow.ToListAsync("SELECT ApplicationId FROM ApiKeyApplications WHERE ApiKeyId=@1",
+                    key);
+
+            var apps = key.Claims
+                .Select(x => int.Parse(x.Value))
+                .ToList();
+            var removed = existingMappings.Except(apps);
+            foreach (var applicationId in removed)
+            {
+                _uow.Execute("DELETE FROM ApiKeyApplications WHERE ApiKeyId = @1 AND ApplicationId = @2",
+                    new[] { key.Id, applicationId });
+            }
+
+            var added = apps.Except(existingMappings);
+            foreach (var id in added)
+            {
+                AddApplication(key.Id, id);
+            }
+        }
+
+        private void AddApplication(int apiKeyId, int applicationId)
+        {
+            _uow.Execute("INSERT INTO [ApiKeyApplications] (ApiKeyId, ApplicationId) VALUES(@api, @app)", new
+            {
+                api = apiKeyId,
+                app = applicationId
+            });
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Commands/CreateApiKeyHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/CreateApiKeyHandler.cs
similarity index 75%
rename from src/Server/OneTrueError.SqlServer/Core/ApiKeys/Commands/CreateApiKeyHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/CreateApiKeyHandler.cs
index 01482512..a89be05e 100644
--- a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Commands/CreateApiKeyHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/CreateApiKeyHandler.cs
@@ -1,56 +1,55 @@
-using System;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using OneTrueError.Api.Core.ApiKeys.Commands;
-using OneTrueError.Api.Core.ApiKeys.Events;
-
-namespace OneTrueError.SqlServer.Core.ApiKeys.Commands
-{
-    [Component(RegisterAsSelf = true)]
-    public class CreateApiKeyHandler : ICommandHandler
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-        private readonly IEventBus _eventBus;
-
-        public CreateApiKeyHandler(IAdoNetUnitOfWork unitOfWork, IEventBus eventBus)
-        {
-            _unitOfWork = unitOfWork;
-            _eventBus = eventBus;
-        }
-
-        public async Task ExecuteAsync(CreateApiKey command)
-        {
-            int id;
-            using (var cmd = _unitOfWork.CreateDbCommand())
-            {
-                cmd.CommandText =
-                    "INSERT INTO ApiKeys (ApplicationName, GeneratedKey, SharedSecret, CreatedById, CreatedAtUtc) VALUES(@appName, @key, @secret, @by, @when); select cast(scope_identity() as int);";
-                cmd.AddParameter("appName", command.ApplicationName);
-                cmd.AddParameter("key", command.ApiKey);
-                cmd.AddParameter("secret", command.SharedSecret);
-                cmd.AddParameter("by", command.AccountId);
-                cmd.AddParameter("when", DateTime.UtcNow);
-                id = (int) await cmd.ExecuteScalarAsync();
-            }
-
-
-            foreach (var applicationId in command.ApplicationIds)
-            {
-                using (var cmd = _unitOfWork.CreateDbCommand())
-                {
-                    cmd.CommandText =
-                        "INSERT INTO ApiKeyApplications (ApiKeyId, ApplicationId) VALUES(@key, @app)";
-                    cmd.AddParameter("app", applicationId);
-                    cmd.AddParameter("key", id);
-                    await cmd.ExecuteNonQueryAsync();
-                }
-            }
-
-            var evt = new ApiKeyCreated(command.ApplicationName, command.ApiKey, command.SharedSecret,
-                command.ApplicationIds, command.AccountId);
-            await _eventBus.PublishAsync(evt);
-        }
-    }
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.ApiKeys.Commands;
+using Coderr.Server.Api.Core.ApiKeys.Events;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.ApiKeys.Commands
+{
+    public class CreateApiKeyHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private readonly IMessageBus _messageBus;
+
+        public CreateApiKeyHandler(IAdoNetUnitOfWork unitOfWork, IMessageBus messageBus)
+        {
+            _unitOfWork = unitOfWork;
+            _messageBus = messageBus;
+        }
+
+        public async Task HandleAsync(IMessageContext context, CreateApiKey command)
+        {
+            int id;
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "INSERT INTO ApiKeys (ApplicationName, GeneratedKey, SharedSecret, CreatedById, CreatedAtUtc) VALUES(@appName, @key, @secret, @by, @when); select cast(scope_identity() as int);";
+                cmd.AddParameter("appName", command.ApplicationName);
+                cmd.AddParameter("key", command.ApiKey);
+                cmd.AddParameter("secret", command.SharedSecret);
+                cmd.AddParameter("by", command.AccountId);
+                cmd.AddParameter("when", DateTime.UtcNow);
+                id = (int) await cmd.ExecuteScalarAsync();
+            }
+
+
+            foreach (var applicationId in command.ApplicationIds)
+            {
+                using (var cmd = _unitOfWork.CreateDbCommand())
+                {
+                    cmd.CommandText =
+                        "INSERT INTO ApiKeyApplications (ApiKeyId, ApplicationId) VALUES(@key, @app)";
+                    cmd.AddParameter("app", applicationId);
+                    cmd.AddParameter("key", id);
+                    await cmd.ExecuteNonQueryAsync();
+                }
+            }
+
+            var evt = new ApiKeyCreated(command.ApplicationName, command.ApiKey, command.SharedSecret,
+                command.ApplicationIds, command.AccountId);
+            await context.SendAsync(evt);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs
similarity index 76%
rename from src/Server/OneTrueError.SqlServer/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs
index af7c85be..c9aaf198 100644
--- a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/DeleteApiKeyHandler.cs
@@ -1,41 +1,40 @@
-using System;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using OneTrueError.Api.Core.ApiKeys.Commands;
-
-namespace OneTrueError.SqlServer.Core.ApiKeys.Commands
-{
-    [Component(RegisterAsSelf = true)]
-    public class DeleteApiKeyHandler : ICommandHandler
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public DeleteApiKeyHandler(IAdoNetUnitOfWork unitOfWork)
-        {
-            if (unitOfWork == null) throw new ArgumentNullException(nameof(unitOfWork));
-            _unitOfWork = unitOfWork;
-        }
-
-        public Task ExecuteAsync(DeleteApiKey command)
-        {
-            int id;
-            if (!string.IsNullOrEmpty(command.ApiKey))
-            {
-                id =
-                    (int)
-                        _unitOfWork.ExecuteScalar("SELECT Id FROM ApiKeys WHERE GeneratedKey = @key",
-                            new {key = command.ApiKey});
-            }
-            else
-            {
-                id = command.Id;
-            }
-
-            _unitOfWork.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @id", new {id});
-            _unitOfWork.ExecuteNonQuery("DELETE FROM [ApiKeys] WHERE Id = @id", new {id});
-            return Task.FromResult(null);
-        }
-    }
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.ApiKeys.Commands;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.ApiKeys.Commands
+{
+    public class DeleteApiKeyHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public DeleteApiKeyHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            if (unitOfWork == null) throw new ArgumentNullException(nameof(unitOfWork));
+            _unitOfWork = unitOfWork;
+        }
+
+        public Task HandleAsync(IMessageContext context, DeleteApiKey command)
+        {
+            int id;
+            if (!string.IsNullOrEmpty(command.ApiKey))
+            {
+                id =
+                    (int)
+                        _unitOfWork.ExecuteScalar("SELECT Id FROM ApiKeys WHERE GeneratedKey = @key",
+                            new {key = command.ApiKey});
+            }
+            else
+            {
+                id = command.Id;
+            }
+
+            _unitOfWork.ExecuteNonQuery("DELETE FROM [ApiKeyApplications] WHERE ApiKeyId = @id", new {id});
+            _unitOfWork.ExecuteNonQuery("DELETE FROM [ApiKeys] WHERE Id = @id", new {id});
+            return Task.FromResult(null);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/EditApiKeyHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/EditApiKeyHandler.cs
new file mode 100644
index 00000000..a01dafac
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Commands/EditApiKeyHandler.cs
@@ -0,0 +1,54 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.ApiKeys.Commands;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.ApiKeys.Commands
+{
+    public class EditApiKeyHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private readonly IMessageBus _messageBus;
+
+        public EditApiKeyHandler(IAdoNetUnitOfWork unitOfWork, IMessageBus messageBus)
+        {
+            _unitOfWork = unitOfWork;
+            _messageBus = messageBus;
+        }
+
+        public async Task HandleAsync(IMessageContext context, EditApiKey command)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "UPDATE ApiKeys SET ApplicationName=@appName WHERE Id = @id";
+                cmd.AddParameter("appName", command.ApplicationName);
+                cmd.AddParameter("id", command.Id);
+                await cmd.ExecuteNonQueryAsync();
+            }
+
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = "DELETE FROM ApiKeyApplications WHERE ApiKeyId = @id";
+                cmd.AddParameter("id", command.Id);
+                await cmd.ExecuteNonQueryAsync();
+            }
+
+            foreach (var applicationId in command.ApplicationIds)
+            {
+                using (var cmd = _unitOfWork.CreateDbCommand())
+                {
+                    cmd.CommandText =
+                        "INSERT INTO ApiKeyApplications (ApiKeyId, ApplicationId) VALUES(@key, @app)";
+                    cmd.AddParameter("app", applicationId);
+                    cmd.AddParameter("key", command.Id);
+                    await cmd.ExecuteNonQueryAsync();
+                }
+            }
+
+            //TODO: Update event?
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/ApiKeyMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/ApiKeyMapper.cs
new file mode 100644
index 00000000..343188cf
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/ApiKeyMapper.cs
@@ -0,0 +1,16 @@
+using Coderr.Server.App.Core.ApiKeys;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.ApiKeys.Mappings
+{
+    public class ApiKeyMapper : CrudEntityMapper
+    {
+        public ApiKeyMapper() : base("ApiKeys")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+            Property(x => x.Claims)
+                .Ignore();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/IntMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/IntMapper.cs
similarity index 83%
rename from src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/IntMapper.cs
rename to src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/IntMapper.cs
index 8ac5e132..390081aa 100644
--- a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/IntMapper.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Mappings/IntMapper.cs
@@ -1,21 +1,21 @@
-using System.Data;
-using Griffin.Data.Mapper;
-
-namespace OneTrueError.SqlServer.Core.ApiKeys.Mappings
-{
-    public class IntMapper : IEntityMapper
-    {
-        public object Create(IDataRecord record)
-        {
-            return record[0];
-        }
-
-        public void Map(IDataRecord source, object destination)
-        {
-        }
-
-        public void Map(IDataRecord source, int destination)
-        {
-        }
-    }
+using System.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.ApiKeys.Mappings
+{
+    public class IntMapper : IEntityMapper
+    {
+        public object Create(IDataRecord record)
+        {
+            return record[0];
+        }
+
+        public void Map(IDataRecord source, object destination)
+        {
+        }
+
+        public void Map(IDataRecord source, int destination)
+        {
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Queries/GetApiKeyHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Queries/GetApiKeyHandler.cs
similarity index 84%
rename from src/Server/OneTrueError.SqlServer/Core/ApiKeys/Queries/GetApiKeyHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Queries/GetApiKeyHandler.cs
index 4bc8d15e..d00df8a3 100644
--- a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Queries/GetApiKeyHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Queries/GetApiKeyHandler.cs
@@ -1,67 +1,64 @@
-using System;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.Api.Core.ApiKeys.Queries;
-using OneTrueError.App.Core.ApiKeys;
-using OneTrueError.SqlServer.Core.ApiKeys.Mappings;
-
-namespace OneTrueError.SqlServer.Core.ApiKeys.Queries
-{
-    /// 
-    ///     Handler for .
-    /// 
-    [Component(RegisterAsSelf = true)]
-    public class GetApiKeyHandler : IQueryHandler
-    {
-        private static readonly MirrorMapper _appMapping =
-            new MirrorMapper();
-
-        private readonly IAdoNetUnitOfWork _uow;
-
-        /// 
-        ///     Creates a new instance of .
-        /// 
-        /// valid uow
-        public GetApiKeyHandler(IAdoNetUnitOfWork uow)
-        {
-            if (uow == null) throw new ArgumentNullException(nameof(uow));
-
-            _uow = uow;
-        }
-
-        /// Method used to execute the query
-        /// Query to execute.
-        /// Task which will contain the result once completed.
-        public async Task ExecuteAsync(GetApiKey query)
-        {
-            if (query == null) throw new ArgumentNullException(nameof(query));
-
-            ApiKey key;
-            if (!string.IsNullOrEmpty(query.ApiKey))
-                key = await _uow.FirstAsync("GeneratedKey=@1", query.ApiKey);
-            else
-                key = await _uow.FirstAsync("Id=@1", query.Id);
-
-            var result = new GetApiKeyResult
-            {
-                ApplicationName = key.ApplicationName,
-                CreatedAtUtc = key.CreatedAtUtc,
-                CreatedById = key.CreatedById,
-                GeneratedKey = key.GeneratedKey,
-                Id = key.Id,
-                SharedSecret = key.SharedSecret
-            };
-
-            var sql = @"SELECT Id as ApplicationId, Name as ApplicationName 
-FROM Applications 
-JOIN ApiKeyApplications ON (Id = ApplicationId)
-WHERE ApiKeyId = @1";
-            var apps = await _uow.ToListAsync(_appMapping, sql, key.Id);
-            result.AllowedApplications = apps.ToArray();
-            return result;
-        }
-    }
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.ApiKeys.Queries;
+using Coderr.Server.App.Core.ApiKeys;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.ApiKeys.Queries
+{
+    /// 
+    ///     Handler for .
+    /// 
+    public class GetApiKeyHandler : IQueryHandler
+    {
+        private static readonly MirrorMapper _appMapping =
+            new MirrorMapper();
+
+        private readonly IAdoNetUnitOfWork _uow;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// valid uow
+        public GetApiKeyHandler(IAdoNetUnitOfWork uow)
+        {
+            if (uow == null) throw new ArgumentNullException(nameof(uow));
+
+            _uow = uow;
+        }
+
+        /// Method used to execute the query
+        /// Query to execute.
+        /// Task which will contain the result once completed.
+        public async Task HandleAsync(IMessageContext context, GetApiKey query)
+        {
+            if (query == null) throw new ArgumentNullException(nameof(query));
+
+            ApiKey key;
+            if (!string.IsNullOrEmpty(query.ApiKey))
+                key = await _uow.FirstAsync("GeneratedKey=@1", query.ApiKey);
+            else
+                key = await _uow.FirstAsync("Id=@1", query.Id);
+
+            var result = new GetApiKeyResult
+            {
+                ApplicationName = key.ApplicationName,
+                CreatedAtUtc = key.CreatedAtUtc,
+                CreatedById = key.CreatedById,
+                GeneratedKey = key.GeneratedKey,
+                Id = key.Id,
+                SharedSecret = key.SharedSecret
+            };
+
+            var sql = @"SELECT Id as ApplicationId, Name as ApplicationName 
+FROM Applications 
+JOIN ApiKeyApplications ON (Id = ApplicationId)
+WHERE ApiKeyId = @1";
+            var apps = await _uow.ToListAsync(_appMapping, sql, key.Id);
+            result.AllowedApplications = apps.ToArray();
+            return result;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Queries/ListApiKeysHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Queries/ListApiKeysHandler.cs
new file mode 100644
index 00000000..3337b0a8
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/ApiKeys/Queries/ListApiKeysHandler.cs
@@ -0,0 +1,28 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.ApiKeys.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.ApiKeys.Queries
+{
+    public class ListApiKeysHandler : IQueryHandler
+    {
+        private readonly MirrorMapper _mapper = new MirrorMapper();
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public ListApiKeysHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, ListApiKeys query)
+        {
+            var keys =
+                await
+                    _unitOfWork.ToListAsync(_mapper,
+                        "SELECT Id, GeneratedKey ApiKey, ApplicationName FROM ApiKeys ORDER BY ApplicationName");
+            return new ListApiKeysResult {Keys = keys.ToArray()};
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroup.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroup.cs
new file mode 100644
index 00000000..4c7a4b05
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroup.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    class ApplicationGroup
+    {
+        public int Id { get; set; }
+        public string Name { get; set; }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMap.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMap.cs
new file mode 100644
index 00000000..a654eeae
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMap.cs
@@ -0,0 +1,9 @@
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    public class ApplicationGroupMap
+    {
+        public int ApplicationGroupId { get; set; }
+        public int ApplicationId { get; set; }
+        public int Id { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMapMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMapMapper.cs
new file mode 100644
index 00000000..a8e3fcdd
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMapMapper.cs
@@ -0,0 +1,12 @@
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    internal class ApplicationGroupMapMapper : CrudEntityMapper
+    {
+        public ApplicationGroupMapMapper() : base("ApplicationGroupMap")
+        {
+            Property(x => x.Id).PrimaryKey(false);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMapper.cs
new file mode 100644
index 00000000..e4a4a1b9
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationGroupMapper.cs
@@ -0,0 +1,13 @@
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    internal class ApplicationGroupMapper : CrudEntityMapper
+    {
+        public ApplicationGroupMapper() : base("ApplicationGroups")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationMapper.cs
new file mode 100644
index 00000000..1a62327e
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationMapper.cs
@@ -0,0 +1,16 @@
+using System;
+using Coderr.Server.Domain.Core.Applications;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    public class ApplicationMapper : CrudEntityMapper
+    {
+        public ApplicationMapper() : base("Applications")
+        {
+            Property(x => x.ApplicationType)
+                .ToPropertyValue(o => (TypeOfApplication) Enum.Parse(typeof(TypeOfApplication), (string) o));
+            Property(x => x.GroupId).Ignore();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationRepository.cs
new file mode 100644
index 00000000..10ca2812
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/ApplicationRepository.cs
@@ -0,0 +1,228 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.Applications;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    [ContainerService]
+    public class ApplicationRepository : IApplicationRepository
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public ApplicationRepository(IAdoNetUnitOfWork uow)
+        {
+            if (uow == null) throw new ArgumentNullException("uow");
+            _uow = uow;
+        }
+
+        public async Task CreateAsync(ApplicationTeamMember member)
+        {
+            await _uow.InsertAsync(member);
+        }
+
+        public async Task GetForUserAsync(int accountId)
+        {
+            if (accountId <= 0) throw new ArgumentOutOfRangeException(nameof(accountId));
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"SELECT a.Id ApplicationId, a.Name ApplicationName, ApplicationMembers.Roles, a.NumberOfFtes NumberOfDevelopers, ag.Id GroupId, ag.Name GroupName
+                                        FROM Applications a
+                                        JOIN ApplicationMembers ON (ApplicationMembers.ApplicationId = a.Id) 
+                                        JOIN ApplicationGroupMap agm ON (agm.ApplicationId = a.Id)
+                                        JOIN ApplicationGroups ag ON (ag.Id = agm.ApplicationGroupId)
+                                        WHERE ApplicationMembers.AccountId = @userId
+                                        ORDER BY ag.Name, a.Name";
+                cmd.AddParameter("userId", accountId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var apps = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var numberOfDevelopers = reader.GetValue(3);
+                        var a = new UserApplication
+                        {
+                            IsAdmin = reader.GetString(2).Contains("Admin"),
+                            ApplicationName = reader.GetString(1),
+                            ApplicationId = reader.GetInt32(0),
+                            NumberOfDevelopers = numberOfDevelopers is DBNull ? null : (decimal?)numberOfDevelopers
+                        };
+                        apps.Add(a);
+                    }
+
+                    return apps.ToArray();
+                }
+            }
+        }
+
+        public async Task RemoveTeamMemberAsync(int applicationId, string invitedEmailAddress)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText = "DELETE FROM ApplicationMembers WHERE ApplicationId=@appId AND EmailAddress = @email";
+                cmd.AddParameter("appId", applicationId);
+                cmd.AddParameter("email", invitedEmailAddress);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task UpdateAsync(ApplicationTeamMember member)
+        {
+            await _uow.UpdateAsync(member);
+        }
+
+        public async Task> GetTeamMembersAsync(int applicationId)
+        {
+            return await _uow.ToListAsync(@"SELECT Users.UserName, ApplicationMembers.* 
+                                                                    FROM ApplicationMembers 
+                                                                    LEFT JOIN Users ON (Users.AccountId = ApplicationMembers.AccountId) 
+                                                                    WHERE ApplicationId = @1", applicationId);
+        }
+
+
+        public async Task GetByKeyAsync(string appKey)
+        {
+            if (appKey == null) throw new ArgumentNullException("appKey");
+
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "SELECT * FROM Applications WHERE AppKey = @id";
+
+                cmd.AddParameter("id", appKey);
+                var item = await cmd.FirstOrDefaultAsync(new ApplicationMapper());
+                if (item == null)
+                    throw new EntityNotFoundException(appKey, cmd);
+                return item;
+            }
+        }
+
+        /*Id uniqueidentifier not null primary key,
+	Title nvarchar(50) not null,
+	AppKey uniqueidentifier not null,
+	OrganizationId uniqueidentifier not null,
+	CreatedById uniqueidentifier not null,
+	CreatedAtUtc datetime2 not null,
+	ApplicationType varchar(40) not null,
+	SharedSecret uniqueidentifier not null,*/
+
+        public async Task GetByIdAsync(int id)
+        {
+            if (id == 0)
+                throw new ArgumentNullException("id");
+
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "SELECT * FROM Applications WHERE Id = @id";
+
+                cmd.AddParameter("id", id);
+                var item = await cmd.FirstOrDefaultAsync();
+                if (item == null)
+                    throw new EntityNotFoundException("Failed to find application with id " + id, cmd);
+
+                return item;
+            }
+        }
+
+        public async Task CreateAsync(Application application)
+        {
+            if (application == null) throw new ArgumentNullException(nameof(application));
+
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"INSERT INTO Applications (Name, AppKey, CreatedById, CreatedAtUtc, ApplicationType, SharedSecret, EstimatedNumberOfErrors, NumberOfFtes) 
+                        VALUES(@Name, @AppKey, @CreatedById, @CreatedAtUtc, @ApplicationType, @SharedSecret, @EstimatedNumberOfErrors, @NumberOfFtes);SELECT SCOPE_IDENTITY();";
+                cmd.AddParameter("Name", application.Name);
+                cmd.AddParameter("AppKey", application.AppKey);
+                cmd.AddParameter("CreatedById", application.CreatedById);
+                cmd.AddParameter("CreatedAtUtc", application.CreatedAtUtc);
+                cmd.AddParameter("ApplicationType", application.ApplicationType.ToString());
+                cmd.AddParameter("SharedSecret", application.SharedSecret);
+                cmd.AddParameter("EstimatedNumberOfErrors", application.EstimatedNumberOfErrors);
+                cmd.AddParameter("NumberOfFtes", application.NumberOfFtes);
+                var item = (decimal)await cmd.ExecuteScalarAsync();
+                application.GetType().GetProperty("Id").SetValue(application, (int)item);
+            }
+
+            await MapGroup(application);
+        }
+
+
+        public async Task DeleteAsync(int applicationId)
+        {
+            if (applicationId == 0) throw new ArgumentNullException("applicationId");
+
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "DELETE FROM Incidents WHERE ApplicationId=@id;\r\n" +
+                    "DELETE FROM Applications WHERE Id = @id";
+
+                //TODO: Delete reports??
+                // or save them for future analysis?
+
+                cmd.AddParameter("id", applicationId);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task GetAllAsync()
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText = "SELECT * FROM Applications ORDER BY Name";
+
+                //cmd.AddParameter("ids", string.Join(", ", appIds.Select(x => "'" + x + "'")));
+                var result = await cmd.ToListAsync();
+                return result.ToArray();
+            }
+        }
+
+        public async Task UpdateAsync(Application entity)
+        {
+            await _uow.UpdateAsync(entity);
+        }
+
+        public Task GetFirstGroupIdAsync()
+        {
+            var id = (int)_uow.ExecuteScalar("SELECT TOP(1) Id FROM ApplicationGroups");
+            return Task.FromResult(id);
+        }
+
+        public async Task RemoveTeamMemberAsync(int applicationId, int userId)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText = "DELETE FROM ApplicationMembers WHERE ApplicationId=@appId AND AccountId = @userId";
+                cmd.AddParameter("appId", applicationId);
+                cmd.AddParameter("userId", userId);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task DeleteAsync(Application application)
+        {
+            if (application == null) throw new ArgumentNullException("application");
+            await DeleteAsync(application.Id);
+        }
+
+        private async Task MapGroup(Application application)
+        {
+            if (application == null) throw new ArgumentNullException(nameof(application));
+            if (application.GroupId == 0) application.GroupId = await GetFirstGroupIdAsync();
+
+            await _uow.InsertAsync(new ApplicationGroupMap
+            {
+                ApplicationGroupId = application.GroupId, ApplicationId = application.Id
+            });
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/CreateApplicationGroupHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/CreateApplicationGroupHandler.cs
new file mode 100644
index 00000000..b29e0d5f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/CreateApplicationGroupHandler.cs
@@ -0,0 +1,36 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Api.Core.Applications.Commands;
+using Coderr.Server.Api.Core.Applications.Events;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    public class CreateApplicationGroupHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public CreateApplicationGroupHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, CreateApplicationGroup message)
+        {
+            var entry = await _unitOfWork.FirstOrDefaultAsync(new {message.Name});
+            if (entry != null)
+                return;
+
+            var group = new ApplicationGroup
+            {
+                Name = message.Name
+            };
+            await _unitOfWork.InsertAsync(group);
+
+            await context.ReplyAsync(
+                new ApplicationGroupCreated(group.Id, group.Name, context.Principal.GetAccountId()));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/DeleteApplicationGroupHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/DeleteApplicationGroupHandler.cs
new file mode 100644
index 00000000..b9b17cc0
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/DeleteApplicationGroupHandler.cs
@@ -0,0 +1,37 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Applications.Commands;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    public class DeleteApplicationGroupHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public DeleteApplicationGroupHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, DeleteApplicationGroup message)
+        {
+            var moveToId = message.MoveAppsToGroupId;
+            if (message.MoveAppsToGroupId == 0)
+            {
+                moveToId = (int)_unitOfWork.ExecuteScalar("SELECT TOP(1) Id FROM ApplicationGroups ORDER BY Id");
+            }
+
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = "UPDATE ApplicationGroupMap SET ApplicationGroupId = @toGroupId WHERE ApplicationGroupId = @fromGroupId";
+                cmd.AddParameter("toGroupId", moveToId);
+                cmd.AddParameter("fromGroupId", message.GroupId);
+                cmd.ExecuteNonQuery();
+            }
+
+            await _unitOfWork.DeleteAsync(new { Id = message.GroupId });
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/Applications/GetApplicationByAppKeyHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/GetApplicationByAppKeyHandler.cs
similarity index 76%
rename from src/Server/OneTrueError.SqlServer/Core/Applications/GetApplicationByAppKeyHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Applications/GetApplicationByAppKeyHandler.cs
index 97c37b81..7f6fceb7 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Applications/GetApplicationByAppKeyHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/GetApplicationByAppKeyHandler.cs
@@ -1,35 +1,34 @@
-//using System;
-//using System.Threading.Tasks;
-//using Griffin.Container;
-//using OneTrueError.Core.Api.Applications;
-//
-
-//namespace OneTrueError.Data.Applications
-//{
-//    [Component]
-//    public class GetApplicationByAppKeyHandler : IQueryHandler
-//    {
-//        private readonly IAdoNetUnitOfWork _uow;
-
-//        public GetApplicationByAppKeyHandler(SomeUow uow)
-//        {
-//            if (uow == null) throw new ArgumentNullException("uow");
-//            _uow = uow;
-//        }
-
-
-//        public async Task ExecuteAsync(GetApplicationByAppKey query)
-//        {
-//            using (var cmd = _uow.CreateCommand())
-//            {
-//                cmd.CommandText =
-//                    "SELECT * FROM Applications WHERE AppKey = @id";
-
-//                cmd.AddParameter("id", query.AppKey);
-//                var result = await cmd.FirstOrDefaultAsync(new ApplicationMapper());
-//                return new ApplicationResult(result);
-//            }
-//        }
-//    }
-//}
-
+//using System;
+//using System.Threading.Tasks;
+//using Coderr.Server.ReportAnalyzer.Abstractions;
+//using Coderr.Core.Api.Applications;
+//
+
+//namespace Coderr.Data.Applications
+//{
+//    public class GetApplicationByAppKeyHandler : IQueryHandler
+//    {
+//        private readonly IAdoNetUnitOfWork _uow;
+
+//        public GetApplicationByAppKeyHandler(SomeUow uow)
+//        {
+//            if (uow == null) throw new ArgumentNullException("uow");
+//            _uow = uow;
+//        }
+
+
+//        public async Task ExecuteAsync(IMessageContext context, GetApplicationByAppKey query)
+//        {
+//            using (var cmd = _uow.CreateCommand())
+//            {
+//                cmd.CommandText =
+//                    "SELECT * FROM Applications WHERE AppKey = @id";
+
+//                cmd.AddParameter("id", query.AppKey);
+//                var result = await cmd.FirstOrDefaultAsync(new ApplicationMapper());
+//                return new ApplicationResult(result);
+//            }
+//        }
+//    }
+//}
+
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationGroupMapHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationGroupMapHandler.cs
new file mode 100644
index 00000000..af100cdf
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationGroupMapHandler.cs
@@ -0,0 +1,61 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Applications.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Applications.Queries
+{
+    internal class GetApplicationGroupMapHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetApplicationGroupMapHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context,
+            GetApplicationGroupMap query)
+        {
+            if (query.ApplicationId != null)
+            {
+                var item = await _unitOfWork.FirstAsync(new { query.ApplicationId });
+                return new GetApplicationGroupMapResult
+                {
+                    Items = new[]
+                    {
+                        new GetApplicationGroupMapResultItem
+                        {
+                            ApplicationId = item.ApplicationId,
+                            GroupId = item.ApplicationGroupId
+                        }
+                    }
+                };
+            }
+
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"declare @defaultId int;
+                                    select top(1) @defaultId = id from ApplicationGroups order by id;
+
+                                    SELECT agm.Id, a.Id ApplicationId, case when agm.ApplicationGroupId is null then @defaultId else agm.ApplicationGroupId end ApplicationGroupId
+                                    FROM Applications a
+                                    LEFT JOIN ApplicationGroupMap agm ON (agm.ApplicationId=a.Id)";
+
+                var items = await cmd.ToListAsync();
+                return new GetApplicationGroupMapResult
+                {
+                    Items = items.Select(x => new GetApplicationGroupMapResultItem
+                    {
+                        ApplicationId = x.ApplicationId,
+                        GroupId = x.ApplicationGroupId
+                    }).ToArray()
+                };
+
+            }
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationGroupsHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationGroupsHandler.cs
new file mode 100644
index 00000000..013af943
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationGroupsHandler.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Applications.Queries;
+using DotNetCqs;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Applications.Queries
+{
+    internal class GetApplicationGroupsHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetApplicationGroupsHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetApplicationGroups query)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT Id,Name FROM ApplicationGroups";
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var items = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var entry = new GetApplicationGroupsResultItem
+                        {
+                            Id = reader.GetInt32(0),
+                            Name = reader.GetString(1)
+                        };
+                        items.Add(entry);
+                    }
+
+                    return new GetApplicationGroupsResult
+                    {
+                        Items = items.ToArray()
+                    };
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs
new file mode 100644
index 00000000..e2d5ca66
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs
@@ -0,0 +1,32 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Applications.Queries;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Applications.Queries
+{
+    public class GetApplicationIdByKeyHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public GetApplicationIdByKeyHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetApplicationIdByKey query)
+        {
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT Id FROM Applications WHERE AppKey = @appKey";
+                cmd.AddParameter("appKey", query.ApplicationKey);
+                var result = await cmd.ExecuteScalarAsync();
+                if (result == null)
+                    return null;
+
+                return new GetApplicationIdByKeyResult {Id = (int)result };
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationOverviewHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationOverviewHandler.cs
new file mode 100644
index 00000000..3ca86823
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/Queries/GetApplicationOverviewHandler.cs
@@ -0,0 +1,357 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions;
+using Coderr.Server.Api.Core.Applications.Queries;
+using Coderr.Server.Domain.Core.Incidents;
+using DotNetCqs;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Applications.Queries
+{
+    internal class GetApplicationOverviewHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetApplicationOverviewHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetApplicationOverview query)
+        {
+            if (query.NumberOfDays == 0)
+                query.NumberOfDays = 30;
+
+            if (query.NumberOfDays == 1)
+                return await GetTodaysOverviewAsync(query);
+
+            var result = new GetApplicationOverviewResult();
+
+            if (query.IncludeChartData)
+            {
+                await LoadChartData(query, result);
+            }
+            
+            await GetStatSummary(query, result);
+            return result;
+        }
+
+        private async Task LoadChartData(GetApplicationOverview query, GetApplicationOverviewResult result)
+        {
+            var curDate = DateTime.Today.AddDays(-query.NumberOfDays);
+            var errorReports = new Dictionary();
+            var incidents = new Dictionary();
+            while (curDate <= DateTime.Today)
+            {
+                errorReports[curDate] = 0;
+                incidents[curDate] = 0;
+                curDate = curDate.AddDays(1);
+            }
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                string filter1;
+                string filter2;
+                if (query.Version != null)
+                {
+                    var id = _unitOfWork.ExecuteScalar("SELECT Id FROM ApplicationVersions WHERE Version = @version",
+                        new {version = query.Version});
+                    filter1 = @"JOIN IncidentVersions On (Incidents.Id = IncidentVersions.IncidentId)
+                            WHERE IncidentVersions.VersionId = @versionId AND ";
+                    filter2 = @"JOIN IncidentVersions On (IncidentReports.IncidentId = IncidentVersions.IncidentId)
+                            WHERE IncidentVersions.VersionId = @versionId AND ";
+                    cmd.AddParameter("versionId", id);
+                }
+                else
+                {
+                    filter1 = "WHERE ";
+                    filter2 = "WHERE ";
+                }
+
+                var sql = @"select cast(Incidents.CreatedAtUtc as date), count(Incidents.Id)
+from Incidents WITH (ReadUncommitted)
+{2} Incidents.CreatedAtUtc >= @minDate
+AND Incidents.CreatedAtUtc <= GetUtcDate()
+{0}
+group by cast(Incidents.CreatedAtUtc as date);
+select cast(IncidentReports.ReceivedAtUtc as date), count(IncidentReports.Id)
+from IncidentReports WITH (ReadUncommitted)
+join Incidents isa WITH (ReadUncommitted) ON (isa.Id = IncidentReports.IncidentId)
+{3} IncidentReports.ReceivedAtUtc >= @minDate
+AND IncidentReports.ReceivedAtUtc <= GetUtcDate()
+{1}
+group by cast(IncidentReports.ReceivedAtUtc as date);";
+
+                if (query.ApplicationId > 0)
+                {
+                    cmd.CommandText = string.Format(sql,
+                        " AND Incidents.ApplicationId = @appId",
+                        " AND isa.ApplicationId = @appId",
+                        filter1, filter2);
+                    cmd.AddParameter("appId", query.ApplicationId);
+                }
+                else
+                {
+                    cmd.CommandText = string.Format(sql, "", "", filter1, filter2);
+                }
+
+                cmd.AddParameter("minDate", DateTime.Today.AddDays(-query.NumberOfDays));
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        incidents[(DateTime)reader[0]] = (int)reader[1];
+                    }
+
+                    await reader.NextResultAsync();
+                    while (await reader.ReadAsync())
+                    {
+                        errorReports[(DateTime)reader[0]] = (int)reader[1];
+                    }
+
+                    result.ErrorReports = errorReports.Select(x => x.Value).ToArray();
+                    result.Incidents = incidents.Select(x => x.Value).ToArray();
+                    result.TimeAxisLabels = incidents.Select(x => x.Key.ToString("yyyy-MM-dd")).ToArray();
+                }
+            }
+        }
+
+        private async Task GetStatSummary(GetApplicationOverview query, GetApplicationOverviewResult result)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                if (!string.IsNullOrEmpty(query.Version))
+                {
+                    var versionId =
+                        _unitOfWork.ExecuteScalar("SELECT Id FROM ApplicationVersions WHERE Version=@version",
+                            new { version = query.Version });
+                    cmd.CommandText = $@"select count(id), max(CreatedAtUtc) 
+from incidents  WITH (ReadUncommitted)
+JOIN IncidentVersions ON (Incidents.Id = IncidentVersions.IncidentId)
+WHERE IncidentVersions.VersionId = @versionId
+AND CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND ApplicationId = @appId 
+AND Incidents.State <> {(int)IncidentState.Ignored}
+AND Incidents.State <> {(int)IncidentState.Closed};
+
+SELECT count(id), max(ReceivedAtUtc)
+from IncidentReports  WITH (ReadUncommitted)
+JOIN IncidentVersions ON (IncidentReports.IncidentId = IncidentVersions.IncidentId)
+WHERE IncidentVersions.VersionId = @versionId
+AND ReceivedAtUtc >= @minDate
+AND ReceivedAtUtc <= GetUtcDate()
+AND ApplicationId = @appId;
+
+SELECT count(distinct emailaddress) 
+from IncidentFeedback
+JOIN IncidentVersions ON (IncidentFeedback.IncidentId = IncidentVersions.IncidentId)
+WHERE IncidentVersions.VersionId = @versionId
+AND CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND ApplicationId = @appId
+AND emailaddress is not null
+AND DATALENGTH(emailaddress) > 0;
+
+select count(*) 
+from IncidentFeedback 
+JOIN IncidentVersions ON (IncidentFeedback.IncidentId = IncidentVersions.IncidentId)
+WHERE IncidentVersions.VersionId = @versionId
+AND CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND ApplicationId = @appId
+AND Description is not null
+AND DATALENGTH(Description) > 0;";
+                    cmd.AddParameter("versionId", versionId);
+                }
+                else
+                {
+                    cmd.CommandText = $@"select count(id), max(CreatedAtUtc) 
+from incidents  WITH (ReadUncommitted)
+where CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND ApplicationId = @appId 
+AND Incidents.State <> {(int)IncidentState.Ignored}
+AND Incidents.State <> {(int)IncidentState.Closed};
+
+SELECT count(IncidentReports.id), max(ReceivedAtUtc)
+FROM IncidentReports WITH (ReadUncommitted)
+JOIN Incidents ON (Incidents.Id = IncidentReports.IncidentId)
+WHERE ReceivedAtUtc >= @minDate
+AND ReceivedAtUtc <= GetUtcDate()
+AND ApplicationId = @appId;
+
+select count(distinct emailaddress) 
+from IncidentFeedback
+where CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND ApplicationId = @appId
+AND emailaddress is not null
+AND DATALENGTH(emailaddress) > 0;
+
+select count(*) 
+from IncidentFeedback 
+where CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND ApplicationId = @appId
+AND Description is not null
+AND DATALENGTH(Description) > 0;";
+
+                }
+
+                if (query.IncludePartitions)
+                {
+                    cmd.CommandText += @"
+                        select max(pd.Name), max(pd.PartitionKey), partitionid, count(distinct value)
+                        from ApplicationPartitionInsights  api
+                        join PartitionDefinitions pd on (pd.Id = api.PartitionId)
+                        where YearMonth = @yearMonth
+                        group by partitionId";
+                    cmd.AddParameter("yearMonth", new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1));
+                }
+
+                cmd.AddParameter("appId", query.ApplicationId);
+                var minDate = query.NumberOfDays == 1
+                    ? DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23)
+                    : DateTime.Today.AddDays(-query.NumberOfDays);
+                cmd.AddParameter("minDate", minDate);
+
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    if (!await reader.ReadAsync())
+                    {
+                        throw new InvalidOperationException("Expected to be able to read.");
+                    }
+
+                    var value = reader[1];
+                    var data = new OverviewStatSummary
+                    {
+                        Incidents = reader.GetInt32(0),
+                        NewestIncidentReceivedAtUtc = value is DBNull ? null : (DateTime?)value
+                    };
+
+                    await reader.NextResultAsync();
+                    await reader.ReadAsync();
+                    data.Reports = reader.GetInt32(0);
+                    value = reader[1];
+                    data.NewestReportReceivedAtUtc = value is DBNull ? null : (DateTime?)value;
+
+                    await reader.NextResultAsync();
+                    await reader.ReadAsync();
+                    data.Followers = reader.GetInt32(0);
+                    
+                    await reader.NextResultAsync();
+                    await reader.ReadAsync();
+                    data.UserFeedback = reader.GetInt32(0);
+
+                    if (query.IncludePartitions && ServerConfig.Instance.IsCommercial)
+                    {
+                        await reader.NextResultAsync();
+                        var partitions = new List();
+                        while (await reader.ReadAsync())
+                        {
+                            var item = new PartitionOverview
+                            {
+                                Name = reader.GetString(1),
+                                DisplayName = reader.GetString(0),
+                                Value = reader.GetInt32(3)
+                            };
+                            partitions.Add(item);
+                        }
+
+                        data.Partitions = partitions.ToArray();
+                    }
+
+                    result.StatSummary = data;
+                }
+            }
+        }
+
+        private async Task GetTodaysOverviewAsync(GetApplicationOverview query)
+        {
+            var result = new GetApplicationOverviewResult
+            {
+                TimeAxisLabels = new string[24]
+            };
+            var incidentValues = new Dictionary();
+            var reportValues = new Dictionary();
+
+            var startDate = DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23);
+            for (var i = 0; i < 24; i++)
+            {
+                result.TimeAxisLabels[i] = startDate.AddHours(i).ToString("HH:mm");
+                incidentValues[startDate.AddHours(i)] = 0;
+                reportValues[startDate.AddHours(i)] = 0;
+            }
+            var filter1 = "";
+            var filter2 = "";
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                if (query.Version != null)
+                {
+                    var id = _unitOfWork.ExecuteScalar("SELECT Id FROM ApplicationVersions WHERE Version = @version",
+                        new { version = query.Version });
+                    filter1 = @"JOIN IncidentVersions On (Incidents.Id = IncidentVersions.IncidentId)
+                            WHERE IncidentVersions.VersionId = @versionId AND ";
+                    filter2 = @"JOIN IncidentVersions On (IncidentReports.IncidentId = IncidentVersions.IncidentId)
+                            WHERE IncidentVersions.VersionId = @versionId AND ";
+                    cmd.AddParameter("versionId", id);
+                }
+                else
+                {
+                    filter1 = "WHERE ";
+                    filter2 = "WHERE ";
+                }
+
+                var sql = @"SELECT DATEPART(HOUR, Incidents.CreatedAtUtc), cast(count(Id) as int)
+ from Incidents WITH (ReadUncommitted)
+ {0} Incidents.CreatedAtUtc >= @minDate
+ AND Incidents.CreatedAtUtc <= GetUtcDate()
+ AND Incidents.ApplicationId = @appId
+ group by DATEPART(HOUR, Incidents.CreatedAtUtc);
+ select DATEPART(HOUR, IncidentReports.ReceivedAtUtc), cast(count(Id) as int)
+ from IncidentReports WITH (ReadUncommitted)
+ JOIN Incidents ice ON (ice.Id = IncidentId)
+ {1} IncidentReports.ReceivedAtUtc >= @minDate
+ AND IncidentReports.ReceivedAtUtc <= GetUtcDate()
+ AND ice.ApplicationId = @appId
+ group by DATEPART(HOUR, IncidentReports.ReceivedAtUtc);";
+
+                cmd.CommandText = string.Format(sql, filter1, filter2);
+                cmd.AddParameter("appId", query.ApplicationId);
+                cmd.AddParameter("minDate", startDate);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var todayWithHour = DateTime.Today.AddHours(DateTime.Now.Hour);
+                    while (await reader.ReadAsync())
+                    {
+                        var hour = reader.GetInt32(0);
+                        var date = hour < todayWithHour.Hour
+                            ? DateTime.Today.AddHours(hour)
+                            : DateTime.Today.AddDays(-1).AddHours(hour);
+                        incidentValues[date] = reader.GetInt32(1);
+                    }
+                    await reader.NextResultAsync();
+                    while (await reader.ReadAsync())
+                    {
+                        var hour = reader.GetInt32(0);
+                        var date = hour < todayWithHour.Hour
+                            ? DateTime.Today.AddHours(hour)
+                            : DateTime.Today.AddDays(-1).AddHours(hour);
+                        reportValues[date] = reader.GetInt32(1);
+                    }
+                }
+            }
+
+            result.ErrorReports = reportValues.Values.ToArray();
+            result.Incidents = incidentValues.Values.ToArray();
+
+            //a bit weird, but required since the method
+            await GetStatSummary(query, result);
+
+            return result;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Applications/SetApplicationGroupHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Applications/SetApplicationGroupHandler.cs
new file mode 100644
index 00000000..6627de42
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Applications/SetApplicationGroupHandler.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Applications.Commands;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Applications
+{
+    internal class SetApplicationGroupHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public SetApplicationGroupHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, SetApplicationGroup message)
+        {
+            await _unitOfWork.DeleteAsync(new { message.ApplicationId });
+
+            var groupId = message.ApplicationGroupId;
+            if (!string.IsNullOrWhiteSpace(message.GroupName))
+            {
+                var group = await _unitOfWork.FirstOrDefaultAsync(new { Name = message.GroupName });
+                if (group == null)
+                    throw new InvalidOperationException("There is no group called " + message.GroupName);
+
+                groupId = group.Id;
+            }
+
+            var map = new ApplicationGroupMap
+            {
+                ApplicationGroupId = groupId,
+                ApplicationId = message.ApplicationId
+            };
+            await _unitOfWork.InsertAsync(map);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Environments/ApplicationEnvironmentMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Environments/ApplicationEnvironmentMapper.cs
new file mode 100644
index 00000000..26962694
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Environments/ApplicationEnvironmentMapper.cs
@@ -0,0 +1,13 @@
+using Coderr.Server.App.Core.Environments;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Environments
+{
+    public class ApplicationEnvironmentMapper : CrudEntityMapper
+    {
+        public ApplicationEnvironmentMapper() : base("ApplicationEnvironments")
+        {
+            Property(x => x.Name).NotForCrud();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Environments/DeleteAbandonedEnvironments.cs b/src/Server/Coderr.Server.SqlServer/Core/Environments/DeleteAbandonedEnvironments.cs
new file mode 100644
index 00000000..7902a15a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Environments/DeleteAbandonedEnvironments.cs
@@ -0,0 +1,30 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Environments
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class DeleteAbandonedEnvironments : IBackgroundJobAsync
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public DeleteAbandonedEnvironments(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"DELETE FROM IncidentEnvironments
+                                    FROM IncidentEnvironments
+                                    LEFT JOIN Incidents ON (Incidents.Id = IncidentId)
+                                    WHERE Incidents.Id IS NULL";
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Environments/EnvironmentMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Environments/EnvironmentMapper.cs
new file mode 100644
index 00000000..f6520f9f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Environments/EnvironmentMapper.cs
@@ -0,0 +1,13 @@
+using Coderr.Server.App.Core.Environments;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Environments
+{
+    public class EnvironmentMapper : CrudEntityMapper
+    {
+        public EnvironmentMapper() : base("Environments")
+        {
+            Property(x => x.Id).PrimaryKey(true);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Environments/EnvironmentRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Environments/EnvironmentRepository.cs
new file mode 100644
index 00000000..76f255a6
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Environments/EnvironmentRepository.cs
@@ -0,0 +1,156 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.App.Core.Environments;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Environments
+{
+    [ContainerService]
+    internal class EnvironmentRepository : IEnvironmentRepository
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public EnvironmentRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task> ListForApplication(int applicationId)
+        {
+            var sql = @"SELECT ae.*, Name
+                        FROM Environments
+                        JOIN ApplicationEnvironments ae ON (EnvironmentId=Environments.Id)
+                        WHERE ApplicationId = @applicationId";
+
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = sql;
+                cmd.AddParameter("applicationId", applicationId);
+                var items = await cmd.ToListAsync();
+                return items.ToList();
+            }
+        }
+
+        public async Task> ListAll()
+        {
+            string sql;
+            sql = @"select Id, Name from Environments ORDER BY Name";
+
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = sql;
+                var items = await cmd.ToListAsync();
+                return items.ToList();
+            }
+        }
+
+        public async Task Create(Environment environment)
+        {
+            await _unitOfWork.InsertAsync(environment);
+        }
+
+        public async Task Delete(Environment environment)
+        {
+            await _unitOfWork.DeleteAsync(environment);
+        }
+
+        public async Task Create(ApplicationEnvironment environment)
+        {
+            await _unitOfWork.InsertAsync(environment);
+        }
+
+        public async Task Update(ApplicationEnvironment environment)
+        {
+            await _unitOfWork.UpdateAsync(environment);
+        }
+
+        public async Task FindByName(string name)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"select *
+                                    FROM Environments
+                                    WHERE Name = @name";
+                cmd.AddParameter("name", name);
+                return await cmd.FirstOrDefaultAsync();
+            }
+        }
+
+        public async Task Reset(int environmentId, int? applicationId)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                // Start by deleting incidents that are in just our environment.
+                cmd.CommandText = @"WITH JustOurIncidents (IncidentId) AS
+                            (
+	                            select ie.IncidentId
+	                            from IncidentEnvironments ie 
+	                            join Incidents i ON (i.Id = ie.IncidentId)
+	                            join Environments e ON (ie.EnvironmentId = e.Id)
+	                            where i.ApplicationId = @applicationId AND i.State = 0
+	                            group by ie.IncidentId
+	                            having count(e.Id) = 1
+                            )
+                            DELETE Incidents
+                            FROM IncidentEnvironments
+                            JOIN JustOurIncidents ON (JustOurIncidents.IncidentId = IncidentEnvironments.IncidentId)
+                            WHERE IncidentEnvironments.EnvironmentId = @environmentId";
+
+                cmd.AddParameter("environmentId", environmentId);
+                cmd.AddParameter("applicationId", applicationId);
+                await cmd.ExecuteNonQueryAsync();
+            }
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                // Next delete all environment mappings that are for the given environment.
+                cmd.CommandText = @"WITH JustOurIncidents (IncidentId) AS
+                            (
+	                            select ie.IncidentId
+	                            from IncidentEnvironments ie 
+	                            join Incidents i ON (i.Id = ie.IncidentId)
+	                            join Environments e ON (ie.EnvironmentId = e.Id)
+	                            where i.ApplicationId = @applicationId AND i.State = 0
+	                            group by ie.IncidentId
+                            )
+                            DELETE IncidentEnvironments
+                            FROM IncidentEnvironments
+                            JOIN JustOurIncidents ON (JustOurIncidents.IncidentId = IncidentEnvironments.IncidentId)
+                            WHERE IncidentEnvironments.EnvironmentId = @environmentId";
+
+                cmd.AddParameter("environmentId", environmentId);
+                cmd.AddParameter("applicationId", applicationId);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task Find(int environmentId, int applicationId)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"select ae.*, e.Name 
+                                    from ApplicationEnvironments ae 
+                                    JOIN Environments e ON (e.Id = ae.EnvironmentId)
+                                    WHERE EnvironmentId = @envId AND ApplicationId = @appId";
+                cmd.AddParameter("appId", applicationId);
+                cmd.AddParameter("envId", environmentId);
+                return await cmd.FirstOrDefaultAsync();
+            }
+        }
+
+        public async Task Find(int environmentId)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"select *
+                                    FROM Environments
+                                    WHERE Id = @envId";
+                cmd.AddParameter("envId", environmentId);
+                return await cmd.FirstOrDefaultAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Environments/GetEnvironmentsResultItemMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Environments/GetEnvironmentsResultItemMapper.cs
new file mode 100644
index 00000000..68f3eb5b
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Environments/GetEnvironmentsResultItemMapper.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Coderr.Server.Api.Core.Environments.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Environments
+{
+    class GetEnvironmentsResultItemMapper : EntityMapper
+    {
+        
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Feedback/CheckForFeedbackNotificationsToSend.cs b/src/Server/Coderr.Server.SqlServer/Core/Feedback/CheckForFeedbackNotificationsToSend.cs
new file mode 100644
index 00000000..76cda8c9
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Feedback/CheckForFeedbackNotificationsToSend.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Client;
+using Coderr.Server.Abstractions.Config;
+using Coderr.Server.Api.Core.Accounts.Queries;
+using Coderr.Server.Api.Core.Messaging;
+using Coderr.Server.Api.Core.Messaging.Commands;
+using Coderr.Server.Domain.Core.Incidents;
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Coderr.Server.Infrastructure.Configuration;
+using Coderr.Server.ReportAnalyzer.Abstractions.Feedback;
+using Coderr.Server.ReportAnalyzer.UserNotifications;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Handlers;
+using DotNetCqs;
+
+namespace Coderr.Server.SqlServer.Core.Feedback
+{
+    /// 
+    ///     Responsible of sending notifications when a new report have been analyzed.
+    /// 
+    // MUST be here so that it's used from both queues.
+    public class CheckForFeedbackNotificationsToSend :
+        IMessageHandler
+    {
+        private readonly IUserNotificationsRepository _notificationsRepository;
+        private readonly string _baseUrl;
+        private readonly IIncidentRepository _incidentRepository;
+        private readonly INotificationService _notificationService;
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// To load notification configuration
+        public CheckForFeedbackNotificationsToSend(IUserNotificationsRepository notificationsRepository, IConfiguration baseConfig, IIncidentRepository incidentRepository, INotificationService notificationService)
+        {
+            _notificationsRepository = notificationsRepository;
+            _baseUrl = baseConfig.Value.BaseUrl.ToString().Trim('/');
+            _incidentRepository = incidentRepository;
+            _notificationService = notificationService;
+        }
+
+        /// 
+        public async Task HandleAsync(IMessageContext context, FeedbackAttachedToIncident e)
+        {
+            var settings = await _notificationsRepository.GetAllAsync(-1);
+            var incident = await _incidentRepository.GetAsync(e.IncidentId);
+            foreach (var setting in settings)
+            {
+                if (setting.UserFeedback == NotificationState.Disabled)
+                    continue;
+
+                var notificationEmail = await context.QueryAsync(new GetAccountEmailById(setting.AccountId));
+
+                var shortName = incident.Description.Length > 40
+                    ? incident.Description.Substring(0, 40) + "..."
+                    : incident.Description;
+
+                if (string.IsNullOrEmpty(e.UserEmailAddress))
+                    e.UserEmailAddress = "Unknown";
+
+                var incidentUrl = $"{_baseUrl}/discover/{incident.ApplicationId}/incident/{incident.Id}";
+
+                if (setting.UserFeedback == NotificationState.Email)
+                {
+                    await SendEmail(context, e, notificationEmail, shortName, incidentUrl);
+                }
+                else if (setting.UserFeedback == NotificationState.BrowserNotification)
+                {
+                    var msg = $@"Incident: {shortName}
+From: {e.UserEmailAddress}
+Application: {e.ApplicationName}
+{e.Message}";
+                    var notification = new Notification(msg)
+                    {
+                        Title = "New feedback",
+                        Data = new
+                        {
+                            viewFeedbackUrl = $"{incidentUrl}/feedback",
+                            incidentId = e.IncidentId,
+                            applicationId = incident.ApplicationId
+                        },
+                        Timestamp = DateTime.UtcNow,
+                        Actions = new List
+                        {
+                            new NotificationAction
+                            {
+                                Title = "View",
+                                Action = "viewFeedback"
+                            }
+                        }
+                    };
+                    try
+                    {
+                        await _notificationService.SendBrowserNotification(setting.AccountId, notification);
+                    }
+                    catch (Exception ex)
+                    {
+                        Err.Report(ex, new { notification, setting });
+                    }
+
+                }
+
+            }
+        }
+
+        private static async Task SendEmail(IMessageContext context, FeedbackAttachedToIncident e, string notificationEmail,
+            string shortName, string incidentUrl)
+        {
+            //TODO: Add more information
+            var msg = new EmailMessage(notificationEmail)
+            {
+                Subject = "New feedback: " + shortName,
+                TextBody = $@"Incident: {incidentUrl}
+Feedback: {incidentUrl}/feedback
+From: {e.UserEmailAddress}
+
+{e.Message}"
+            };
+
+
+            var emailCmd = new SendEmail(msg);
+            await context.SendAsync(emailCmd);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Feedback/DeleteAbandonedFeedback.cs b/src/Server/Coderr.Server.SqlServer/Core/Feedback/DeleteAbandonedFeedback.cs
new file mode 100644
index 00000000..342d80b7
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Feedback/DeleteAbandonedFeedback.cs
@@ -0,0 +1,30 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Feedback
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class DeleteAbandonedFeedback : IBackgroundJobAsync
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public DeleteAbandonedFeedback(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"DELETE FROM IncidentFeedback
+                                    FROM IncidentFeedback
+                                    LEFT JOIN Incidents ON (Incidents.Id = IncidentId)
+                                    WHERE Incidents.Id IS NULL";
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Feedback/FeedbackEntityMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Feedback/FeedbackEntityMapper.cs
new file mode 100644
index 00000000..1919467e
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Feedback/FeedbackEntityMapper.cs
@@ -0,0 +1,15 @@
+using Coderr.Server.Domain.Core.Feedback;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Feedback
+{
+    public class FeedbackEntityMapper : CrudEntityMapper
+    {
+        public FeedbackEntityMapper() : base("IncidentFeedback")
+        {
+            Property(x => x.ErrorId).ColumnName("ErrorReportId");
+            Property(x => x.CanRemove).Ignore();
+            Property(x => x.CanUpdate).Ignore();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Feedback/FeedbackRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Feedback/FeedbackRepository.cs
new file mode 100644
index 00000000..86d368a9
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Feedback/FeedbackRepository.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.Feedback;
+using Coderr.Server.ReportAnalyzer.Feedback;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Feedback
+{
+    [ContainerService]
+    public class FeedbackRepository : IFeedbackRepository, IUserFeedbackRepository
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public FeedbackRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task FindPendingAsync(string reportId)
+        {
+            return await _unitOfWork.FirstOrDefaultAsync(new { ErrorId = reportId });
+        }
+
+        public async Task UpdateAsync(UserFeedback feedback)
+        {
+            await _unitOfWork.UpdateAsync(feedback);
+        }
+
+        public async Task> GetEmailAddressesAsync(int incidentId)
+        {
+            var emailAddresses = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "SELECT distinct EmailAddress FROM IncidentFeedback WHERE IncidentId = @id AND EmailAddress IS NOT NULL";
+                cmd.AddParameter("id", incidentId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var email = reader.GetString(0);
+                        if (!emailAddresses.Any(x => x.Equals(email, StringComparison.OrdinalIgnoreCase)))
+                            emailAddresses.Add(email);
+                    }
+                }
+            }
+
+            return emailAddresses;
+        }
+
+        public Task CreateAsync(NewFeedback feedback)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Feedback/SubmitFeedbackHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Feedback/SubmitFeedbackHandler.cs
new file mode 100644
index 00000000..ef0f499b
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Feedback/SubmitFeedbackHandler.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Feedback.Commands;
+using Coderr.Server.Domain.Core.Applications;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions.Feedback;
+using DotNetCqs;
+using Griffin.Data;
+using log4net;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.SqlServer.Core.Feedback
+{
+    public class SubmitFeedbackHandler : IMessageHandler
+    {
+        private readonly IApplicationRepository _applicationRepository;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(SubmitFeedbackHandler));
+        private readonly IReportsRepository _reportsRepository;
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public SubmitFeedbackHandler(IAdoNetUnitOfWork unitOfWork, IReportsRepository reportsRepository,
+            IApplicationRepository applicationRepository)
+        {
+            _unitOfWork = unitOfWork;
+            _reportsRepository = reportsRepository;
+            _applicationRepository = applicationRepository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, SubmitFeedback command)
+        {
+            if (string.IsNullOrEmpty(command.Email))
+            {
+                if (string.IsNullOrEmpty(command.Feedback))
+                    return;
+                if (command.Feedback.Length < 3)
+                    return;
+            }
+
+            // A bug where they had switched places.
+            var feedbackGotAt = command.Feedback?.Contains("@") == true;
+            var emailGotAt = command.Email?.Contains("@") == true;
+            if (!emailGotAt && feedbackGotAt)
+            {
+                var tmp = command.Email;
+                command.Email = command.Feedback;
+                command.Feedback = tmp;
+            }
+
+            if (!emailGotAt && !string.IsNullOrEmpty(command.Email) && string.IsNullOrEmpty(command.Feedback))
+            {
+                command.Feedback = command.Email;
+                command.Email = null;
+            }
+
+            _logger.Debug("data " + JsonConvert.SerializeObject(command));
+            ReportMapping report2;
+            int? reportId = null;
+            if (command.ReportId > 0)
+            {
+                var report = await _reportsRepository.GetAsync(command.ReportId);
+                report2 = new ReportMapping
+                {
+                    ApplicationId = report.ApplicationId,
+                    ErrorId = report.ClientReportId,
+                    IncidentId = report.IncidentId,
+                    ReceivedAtUtc = report.CreatedAtUtc
+                };
+                reportId = report.Id;
+            }
+            else
+            {
+                report2 = await _reportsRepository.FindByErrorIdAsync(command.ErrorId);
+                if (report2 == null) _logger.Warn("Failed to find report by error id: " + command.ErrorId);
+            }
+
+            // storing it without connections as the report might not have been uploaded yet.
+            if (report2 == null)
+            {
+                _logger.InfoFormat(
+                    "Failed to find report. Let's enqueue it instead for report {0}/{1}. Email: {2}, Feedback: {3}",
+                    command.ReportId, command.ErrorId, command.Email, command.Feedback);
+                try
+                {
+                    using (var cmd = _unitOfWork.CreateCommand())
+                    {
+                        cmd.CommandText =
+                            "INSERT INTO IncidentFeedback (ErrorReportId, RemoteAddress, Description, EmailAddress, CreatedAtUtc, Conversation, ConversationLength) "
+                            +
+                            "VALUES (@ErrorReportId, @RemoteAddress, @Description, @EmailAddress, @CreatedAtUtc, '', 0)";
+                        cmd.AddParameter("ErrorReportId", command.ErrorId);
+                        cmd.AddParameter("RemoteAddress", command.RemoteAddress);
+                        cmd.AddParameter("Description", command.Feedback ?? "");
+                        cmd.AddParameter("EmailAddress", command.Email);
+                        cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow);
+                        cmd.ExecuteNonQuery();
+                    }
+
+                    _logger.Info("** STORING FEEDBACK");
+                }
+                catch (Exception exception)
+                {
+                    _logger.Error(
+                        $"{command.ErrorId}: Failed to store '{command.Email}' '{command.Feedback}'", exception);
+                    //hide errors.
+                }
+
+                return;
+            }
+
+            using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "INSERT INTO IncidentFeedback (ErrorReportId, ApplicationId, ReportId, IncidentId, RemoteAddress, Description, EmailAddress, CreatedAtUtc, Conversation, ConversationLength) "
+                    +
+                    "VALUES (@ErrorReportId, @ApplicationId, @ReportId, @IncidentId, @RemoteAddress, @Description, @EmailAddress, @CreatedAtUtc, @Conversation, 0)";
+                cmd.AddParameter("ErrorReportId", command.ErrorId);
+                cmd.AddParameter("ApplicationId", report2.ApplicationId);
+                cmd.AddParameter("ReportId", reportId);
+                cmd.AddParameter("IncidentId", report2.IncidentId);
+                cmd.AddParameter("RemoteAddress", command.RemoteAddress);
+                cmd.AddParameter("Description", command.Feedback ?? "");
+                cmd.AddParameter("EmailAddress", command.Email);
+                cmd.AddParameter("Conversation", "");
+                cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow);
+
+                var app = await _applicationRepository.GetByIdAsync(report2.ApplicationId);
+                var evt = new FeedbackAttachedToIncident
+                {
+                    ApplicationId = report2.ApplicationId,
+                    ApplicationName = app.Name,
+                    Message = command.Feedback,
+                    UserEmailAddress = command.Email,
+                    IncidentId = report2.IncidentId
+                };
+                await context.SendAsync(evt);
+
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/DeleteAbandonedFeedback.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/DeleteAbandonedFeedback.cs
new file mode 100644
index 00000000..181857e6
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/DeleteAbandonedFeedback.cs
@@ -0,0 +1,30 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Incidents
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class DeleteAbandonedTags : IBackgroundJobAsync
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public DeleteAbandonedTags(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"DELETE FROM IncidentTags
+                                    FROM IncidentTags
+                                    LEFT JOIN Incidents ON (Incidents.Id = IncidentId)
+                                    WHERE Incidents.Id IS NULL";
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/DeleteAbandonedSimilarities.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/DeleteAbandonedSimilarities.cs
new file mode 100644
index 00000000..fc75a806
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/DeleteAbandonedSimilarities.cs
@@ -0,0 +1,30 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Incidents
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class DeleteAbandonedCorrelations : IBackgroundJobAsync
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public DeleteAbandonedCorrelations(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"DELETE FROM IncidentContextCollections
+                                    FROM IncidentContextCollections
+                                    LEFT JOIN Incidents ON (Incidents.Id = IncidentId)
+                                    WHERE Incidents.Id IS NULL";
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentMapper.cs
new file mode 100644
index 00000000..862dff7e
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentMapper.cs
@@ -0,0 +1,38 @@
+using Coderr.Server.Domain.Core.Incidents;
+using Coderr.Server.SqlServer.Tools;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents
+{
+    public class IncidentMapper : CrudEntityMapper
+    {
+        public IncidentMapper() : base("Incidents")
+        {
+            Property(x => x.SolvedAtUtc)
+                .ToColumnValue(DbConverters.ToNullableSqlDate)
+                .ToPropertyValue(DbConverters.ToEntityDate);
+
+            Property(x => x.LastSolutionAtUtc)
+                .ToColumnValue(DbConverters.ToNullableSqlDate)
+                .ToPropertyValue(DbConverters.ToEntityDate);
+
+            Property(x => x.IgnoringReportsSinceUtc)
+                .ToColumnValue(DbConverters.ToNullableSqlDate)
+                .ToPropertyValue(DbConverters.ToEntityDate);
+
+            Property(x => x.ReopenedAtUtc)
+                .ToColumnValue(DbConverters.ToNullableSqlDate)
+                .ToPropertyValue(DbConverters.ToEntityDate);
+
+            Property(x => x.Solution)
+                .ToColumnValue(DbConverters.SerializeEntity)
+                .ToPropertyValue(EntitySerializer.Deserialize);
+
+            Property(x => x.State);
+            Property(x => x.Escalation).ColumnName("EscalationState");
+
+            Property(x => x.IsSolutionShared)
+                .ToPropertyValue(DbConverters.BoolFromByteArray);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentRepository.cs
new file mode 100644
index 00000000..cb642c65
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentRepository.cs
@@ -0,0 +1,167 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.Incidents;
+using Coderr.Server.SqlServer.Tools;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents
+{
+    [ContainerService]
+    public class IncidentRepository : IIncidentRepository
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public IncidentRepository(IAdoNetUnitOfWork uow)
+        {
+            if (uow == null) throw new ArgumentNullException("uow");
+
+            _uow = uow;
+        }
+
+        public async Task GetLatestIncidentDate(int applicationId)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"SELECT Max(CreatedAtUtc) FROM Incidents WHERE ApplicationId = @ApplicationId AND State < 2";
+                cmd.AddParameter("ApplicationId", applicationId);
+                var value = await cmd.ExecuteScalarAsync();
+                if (value is DBNull)
+                    return DateTime.MinValue;
+
+                return (DateTime)value;
+            }
+        }
+
+        public async Task UpdateAsync(Incident incident)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"UPDATE Incidents SET 
+                        ApplicationId = @ApplicationId,
+                        UpdatedAtUtc = @UpdatedAtUtc,
+                        Description = @Description,
+                        Solution = @Solution,
+                        SolvedAtUtc = @solvedAt,
+                        IsSolutionShared = @IsSolutionShared,
+                        AssignedToId = @AssignedTo,
+                        AssignedAtUtc = @AssignedAtUtc,
+                        State = @state,
+                        IgnoringReportsSinceUtc = @IgnoringReportsSinceUtc,
+                        IgnoringRequestedBy = @IgnoringRequestedBy,
+                        IgnoredUntilVersion = @IgnoredUntilVersion
+                        WHERE Id = @id";
+                cmd.AddParameter("Id", incident.Id);
+                cmd.AddParameter("ApplicationId", incident.ApplicationId);
+                cmd.AddParameter("UpdatedAtUtc", incident.UpdatedAtUtc);
+                cmd.AddParameter("Description", incident.Description);
+                cmd.AddParameter("State", (int)incident.State);
+                cmd.AddParameter("AssignedTo", incident.AssignedToId);
+                cmd.AddParameter("AssignedAtUtc", (object)incident.AssignedAtUtc ?? DBNull.Value);
+                cmd.AddParameter("solvedAt", incident.SolvedAtUtc.ToDbNullable());
+                cmd.AddParameter("IgnoringReportsSinceUtc", incident.IgnoringReportsSinceUtc.ToDbNullable());
+                cmd.AddParameter("IgnoringRequestedBy", incident.IgnoringRequestedBy);
+                cmd.AddParameter("Solution",
+                    incident.Solution == null ? null : EntitySerializer.Serialize(incident.Solution));
+                cmd.AddParameter("IsSolutionShared", incident.IsSolutionShared);
+                cmd.AddParameter("IgnoredUntilVersion", incident.IgnoredUntilVersion);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public Task> GetAll(IEnumerable incidentIds)
+        {
+            if (incidentIds == null) throw new ArgumentNullException(nameof(incidentIds));
+            var ids = string.Join(",", incidentIds);
+            if (ids == "")
+                throw new ArgumentException("No incident IDs were specified.", nameof(incidentIds));
+
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    $"SELECT * FROM Incidents WHERE Id IN ({ids})";
+                return cmd.ToListAsync(new IncidentMapper());
+            }
+        }
+
+        public async Task MapCorrelationId(int incidentId, string correlationId)
+        {
+            var sql = @"declare @id int;
+                        select @id = Id FROM CorrelationIds WHERE Value = @value;
+                        if (@id is NULL)
+                        BEGIN
+                            INSERT INTO CorrelationIds(Value) VALUES(@value);
+                            set @id = scope_identity();
+                        END;
+                        BEGIN TRY
+                          INSERT INTO IncidentCorrelations (CorrelationId, IncidentId) VALUES (@id, @incidentId);  
+                        END TRY
+                        BEGIN CATCH
+                          IF ERROR_NUMBER() NOT IN (2601, 2627) 
+                            THROW;
+                        END CATCH";
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText = sql;
+                cmd.AddParameter("value", correlationId);
+                cmd.AddParameter("incidentId", incidentId);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task GetTotalCountForAppInfoAsync(int applicationId)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"SELECT CAST(count(*) as int) FROM Incidents WHERE ApplicationId = @ApplicationId AND State IN (0,1)";
+                cmd.AddParameter("ApplicationId", applicationId);
+                var result = (int)await cmd.ExecuteScalarAsync();
+                return result;
+            }
+        }
+
+        public Task> GetManyAsync(IEnumerable incidentIds)
+        {
+            if (incidentIds == null) throw new ArgumentNullException(nameof(incidentIds));
+            var ids = string.Join(",", incidentIds);
+            if (ids == "")
+                throw new ArgumentException("No incident IDs were specified.", nameof(incidentIds));
+
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    $"SELECT * FROM Incidents WHERE Id IN ({ids})";
+                return cmd.ToListAsync(new IncidentMapper());
+            }
+        }
+
+        public async Task Delete(int incidentId)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"DELETE FROM Incidents WHERE Id = @id";
+                cmd.AddParameter("Id", incidentId);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public Task GetAsync(int id)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    "SELECT TOP 1 * FROM Incidents WHERE Id = @id";
+
+                cmd.AddParameter("id", id);
+                return cmd.FirstAsync(new IncidentMapper());
+            }
+        }
+    }
+}
diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentSummaryMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentSummaryMapper.cs
similarity index 88%
rename from src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentSummaryMapper.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentSummaryMapper.cs
index 7cbf34b6..14ddc869 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentSummaryMapper.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/IncidentSummaryMapper.cs
@@ -1,29 +1,29 @@
-using System;
-using System.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.Api.Core.Incidents;
-
-namespace OneTrueError.SqlServer.Core.Incidents
-{
-    public class IncidentSummaryMapper : IEntityMapper
-    {
-        public object Create(IDataRecord record)
-        {
-            return new IncidentSummaryDTO((int) record["Id"], (string) record["Description"]);
-        }
-
-        public void Map(IDataRecord source, object destination)
-        {
-            Map(source, (IncidentSummaryDTO) destination);
-        }
-
-        public void Map(IDataRecord source, IncidentSummaryDTO destination)
-        {
-            destination.ApplicationId = (int) source["ApplicationId"];
-            destination.ApplicationName = (string) source["ApplicationName"];
-            destination.ReportCount = (int) source["Count"];
-            destination.LastUpdateAtUtc = (DateTime) source["UpdatedAtUtc"];
-            destination.CreatedAtUtc = (DateTime) source["CreatedAtUtc"];
-        }
-    }
+using System;
+using System.Data;
+using Coderr.Server.Api.Core.Incidents;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents
+{
+    public class IncidentSummaryMapper : IEntityMapper
+    {
+        public object Create(IDataRecord record)
+        {
+            return new IncidentSummaryDTO((int) record["Id"], (string) record["Description"]);
+        }
+
+        public void Map(IDataRecord source, object destination)
+        {
+            Map(source, (IncidentSummaryDTO) destination);
+        }
+
+        public void Map(IDataRecord source, IncidentSummaryDTO destination)
+        {
+            destination.ApplicationId = (int) source["ApplicationId"];
+            destination.ApplicationName = (string) source["ApplicationName"];
+            destination.ReportCount = (int) source["Count"];
+            destination.LastUpdateAtUtc = (DateTime) source["UpdatedAtUtc"];
+            destination.CreatedAtUtc = (DateTime) source["CreatedAtUtc"];
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs
new file mode 100644
index 00000000..b0e5b225
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Data;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    public class FindIncidentResultItemMapper : IEntityMapper
+    {
+        public object Create(IDataRecord record)
+        {
+            return new FindIncidentsResultItem((int) record["Id"], (string) record["Description"]);
+        }
+
+        public void Map(IDataRecord source, object destination)
+        {
+            Map(source, (FindIncidentsResultItem) destination);
+        }
+
+        public void Map(IDataRecord source, FindIncidentsResultItem destination)
+        {
+            destination.ApplicationName = (string) source["ApplicationName"];
+            destination.ApplicationId = (int)source["ApplicationId"];
+            destination.IsReOpened = source["IsReopened"].Equals(1);
+            destination.ReportCount = (int) source["ReportCount"];
+            destination.CreatedAtUtc = (DateTime)source["CreatedAtUtc"];
+            var value = source["UpdatedAtUtc"];
+            if (!(value is DBNull))
+                destination.LastUpdateAtUtc = (DateTime) value;
+
+            value = source["LastReportAtUtc"];
+            destination.LastReportReceivedAtUtc = (DateTime) (value is DBNull ? destination.LastUpdateAtUtc : value);
+
+            value = source["AssignedAtUtc"];
+            destination.AssignedAtUtc = (DateTime?)(value is DBNull ? null : value);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentsHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentsHandler.cs
new file mode 100644
index 00000000..f761b1eb
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/FindIncidentsHandler.cs
@@ -0,0 +1,254 @@
+using System;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Api.Core.Incidents;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using Coderr.Server.Domain.Core.Incidents;
+using Coderr.Server.Infrastructure.Security;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    public class FindIncidentsHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+        private string _where = "";
+        private string _joins = "";
+
+        public FindIncidentsHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        private void AppendWhere(string constraint)
+        {
+            if (_where == "")
+                _where = " WHERE " + constraint + "\r\n";
+            else
+                _where += " AND " + constraint + "\r\n";
+        }
+
+        private void AppendJoin(string clause)
+        {
+            _joins += clause + "\r\n";
+        }
+
+        public async Task HandleAsync(IMessageContext context, FindIncidents query)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                var sqlQuery = @"SELECT {0}
+                                    FROM Incidents 
+                                    JOIN Applications ON (Applications.Id = Incidents.ApplicationId)";
+
+                if (!string.IsNullOrEmpty(query.Version))
+                {
+                    var versionId =
+                        _uow.ExecuteScalar("SELECT Id FROM ApplicationVersions WHERE Version = @version",
+                            new { version = query.Version });
+
+                    AppendJoin("JOIN IncidentVersions ON (Incidents.Id = IncidentVersions.IncidentId)");
+                    AppendWhere("IncidentVersions.VersionId = @versionId");
+                    cmd.AddParameter("versionId", versionId);
+                }
+                if (query.Tags != null && query.Tags.Length > 0)
+                {
+                    var ourSql = @" join IncidentTags on (Incidents.Id=IncidentTags.IncidentId AND IncidentTags.Id IN (
+                                    SELECT MAX(IncidentTags.Id)
+                                    FROM IncidentTags 
+                                    WHERE TagName IN ({0}) 
+                                    AND IncidentTags.IncidentId=Incidents.Id
+                                    GROUP BY IncidentId 
+                                    HAVING Count(IncidentTags.Id) = {1}
+                                    ))";
+                    var ps = "";
+                    for (var i = 0; i < query.Tags.Length; i++)
+                    {
+                        ps += $"@tag{i}, ";
+                        cmd.AddParameter($"@tag{i}", query.Tags[i]);
+                    }
+
+                    var sql = string.Format(ourSql, ps.Remove(ps.Length - 2, 2), query.Tags.Length);
+                    AppendJoin(sql);
+                }
+
+                if (query.EnvironmentIds != null && query.EnvironmentIds.Length > 0)
+                {
+                    AppendJoin("JOIN IncidentEnvironments ON (Incidents.Id = IncidentEnvironments.IncidentId)");
+                    AppendWhere($"IncidentEnvironments.EnvironmentId IN ({string.Join(", ", query.EnvironmentIds)})");
+                }
+
+                if (!string.IsNullOrEmpty(query.ContextCollectionPropertyValue)
+                    || !string.IsNullOrEmpty(query.ContextCollectionName)
+                    || !string.IsNullOrEmpty(query.ContextCollectionPropertyName))
+                {
+                    var where = AddContextProperty(cmd, "", "Name", "ContextName", query.ContextCollectionName);
+                    where = AddContextProperty(cmd, where, "PropertyName", "ContextPropertyName", query.ContextCollectionPropertyName);
+                    where = AddContextProperty(cmd, where, "Value", "ContextPropertyValue", query.ContextCollectionPropertyValue);
+                    if (where.EndsWith(" AND "))
+                        where = where.Substring(0, where.Length - 5);
+                    var ourSql =
+                        $@"with ContextSearch (IncidentId) 
+                        as (
+                            select distinct(IncidentId)
+                            from ErrorReports
+                            join ErrorReportCollectionProperties ON (ErrorReports.Id = ErrorReportCollectionProperties.ReportId)
+                            WHERE {where}
+                        )";
+                    sqlQuery = ourSql + sqlQuery;
+                    AppendJoin("join ContextSearch ON (Incidents.Id = ContextSearch.IncidentId)");
+                }
+
+                if (query.ApplicationIds != null && query.ApplicationIds.Length > 0)
+                {
+                    foreach (var applicationId in query.ApplicationIds)
+                    {
+                        if (!context.Principal.IsApplicationMember(applicationId)
+                            && !context.Principal.IsSysAdmin()
+                            && !context.Principal.IsApplicationAdmin(applicationId))
+                            throw new UnauthorizedAccessException(
+                                "You are not a member of application " + applicationId);
+                    }
+
+                    var ids = string.Join(",", query.ApplicationIds);
+                    AppendWhere($"Applications.Id IN ({ids})");
+                }
+                else if (!context.Principal.IsSysAdmin())
+                {
+                    var appIds = context.Principal.Claims
+                        .Where(x => x.Type == CoderrClaims.Application)
+                        .Select(x => x.Value)
+                        .ToArray();
+                    if (!appIds.Any())
+                    {
+                        return new FindIncidentsResult { Items = new FindIncidentsResultItem[0] };
+                    }
+
+                    AppendWhere($"Applications.Id IN({string.Join(",", appIds)})");
+                }
+
+                if (!string.IsNullOrWhiteSpace(query.FreeText))
+                {
+                    AppendWhere(@"(
+                                    Incidents.Id IN 
+                                    (
+                                        SELECT Distinct IncidentId 
+                                        FROM ErrorReports 
+                                        WHERE StackTrace LIKE @FreeText 
+                                            OR ErrorReports.Title LIKE @FreeText
+                                            OR ErrorReports.ErrorId LIKE @FreeText
+                                            OR Incidents.Description LIKE @FreeText)
+                                    )");
+                    cmd.AddParameter("FreeText", $"%{query.FreeText}%");
+                }
+
+
+
+                _where += " AND (";
+                if (query.IsIgnored)
+                    _where += $"State = {(int)IncidentState.Ignored} OR ";
+                if (query.IsNew)
+                    _where += $"State = {(int)IncidentState.New} OR ";
+                if (query.IsClosed)
+                    _where += $"State = {(int)IncidentState.Closed} OR ";
+                if (query.IsAssigned)
+                    _where += $"State = {(int)IncidentState.Active} OR ";
+                if (query.ReOpened)
+                    _where += "IsReOpened = 1 OR ";
+
+
+                if (_where.EndsWith("OR "))
+                    _where = _where.Remove(_where.Length - 4) + ") ";
+                else
+                    _where = _where.Remove(_where.Length - 5);
+
+                if (query.MinDate > DateTime.MinValue)
+                {
+                    AppendWhere("Incidents.LastReportAtUtc >= @minDate");
+                    cmd.AddParameter("minDate", query.MinDate);
+                }
+                if (query.MaxDate < DateTime.MaxValue)
+                {
+                    AppendWhere("Incidents.LastReportAtUtc <= @maxDate");
+                    cmd.AddParameter("maxDate", query.MaxDate);
+                }
+
+                if (query.AssignedToId > 0)
+                {
+                    AppendWhere("AssignedToId = @assignedTo");
+                    cmd.AddParameter("assignedTo", query.AssignedToId);
+                }
+
+
+
+                //count first;
+                sqlQuery += _joins + _where;
+                cmd.CommandText = string.Format(sqlQuery, "count(Incidents.Id)");
+                var count = await cmd.ExecuteScalarAsync();
+
+
+                // then items
+                if (query.SortType == IncidentOrder.Newest)
+                {
+                    if (query.SortAscending)
+                        sqlQuery += " ORDER BY CreatedAtUtc";
+                    else
+                        sqlQuery += " ORDER BY CreatedAtUtc DESC";
+                }
+                else if (query.SortType == IncidentOrder.LatestReport)
+                {
+                    if (query.SortAscending)
+                        sqlQuery += " ORDER BY LastReportAtUtc";
+                    else
+                        sqlQuery += " ORDER BY LastReportAtUtc DESC";
+                }
+                else if (query.SortType == IncidentOrder.MostReports)
+                {
+                    if (query.SortAscending)
+                        sqlQuery += " ORDER BY ReportCount";
+                    else
+                        sqlQuery += " ORDER BY ReportCount DESC";
+                }
+                cmd.CommandText = string.Format(sqlQuery,
+                    "Incidents.*, Applications.Id as ApplicationId, Applications.Name as ApplicationName");
+                if (query.PageNumber > 0)
+                {
+                    var offset = (query.PageNumber - 1) * query.ItemsPerPage;
+                    cmd.CommandText += $@" OFFSET {offset} ROWS FETCH NEXT {query.ItemsPerPage} ROWS ONLY";
+                }
+                var items = await cmd.ToListAsync();
+
+                return new FindIncidentsResult
+                {
+                    Items = items.ToArray(),
+                    PageNumber = query.PageNumber,
+                    PageSize = query.ItemsPerPage,
+                    TotalCount = (int)count
+                };
+            }
+        }
+
+        protected string AddContextProperty(DbCommand cmd, string sql, string sqlColumnName, string sqlParameterName, string value)
+        {
+            if (string.IsNullOrEmpty(value))
+                return "";
+
+            if (value.Contains("*"))
+            {
+                sql += $" {sqlColumnName} LIKE @{sqlParameterName}";
+                cmd.AddParameter(sqlParameterName, value.Replace("*", "%"));
+            }
+            else
+            {
+                sql += $" {sqlColumnName} = @{sqlParameterName}";
+                cmd.AddParameter(sqlParameterName, value);
+            }
+            return sql + " AND ";
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetCollectionHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetCollectionHandler.cs
new file mode 100644
index 00000000..f11452d4
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetCollectionHandler.cs
@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    internal class GetCollectionHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private ILog _logger = LogManager.GetLogger(typeof(GetCollectionHandler));
+
+        public GetCollectionHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetCollection query)
+        {
+            if (query.MaxNumberOfCollections == 0)
+                query.MaxNumberOfCollections = 1;
+
+            var sql = @"WITH ReportsWithCollection (ErrorReportId)
+                        AS
+                        (
+                            select distinct TOP(10) ErrorReports.Id
+                            FROM ErrorReports WITH (NoLock)
+                            JOIN ErrorReportCollectionProperties ep WITH (NoLock) ON (ep.ReportId = ErrorReports.Id)
+                            WHERE ep.Name = @collectionName
+                            AND ErrorReports.IncidentId=@incidentId
+                        )
+
+                        select erp.PropertyName, erp.Value, ErrorReports.CreatedAtUtc, ErrorReports.Id ReportId
+                        from ErrorReportCollectionProperties erp  WITH (NoLock)
+                        join ReportsWithCollection rc WITH (NoLock) on (erp.ReportId = rc.ErrorReportId)
+                        join ErrorReports WITH (NoLock) on (ErrorReports.ID = rc.ErrorReportId)
+                        WHERE erp.Name = @collectionName";
+
+            var items = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = sql;
+                cmd.AddParameter("incidentId", query.IncidentId);
+                cmd.AddParameter("collectionName", query.CollectionName);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    GetCollectionResultItem item = null;
+                    var lastReportId = 0;
+                    while (reader.Read())
+                    {
+                        var reportId = (int)reader["ReportId"];
+                        if (reportId != lastReportId || item == null)
+                        {
+                            item = new GetCollectionResultItem
+                            {
+                                ReportId = (int)reader["ReportId"],
+                                ReportDate = (DateTime)reader["CreatedAtUtc"],
+                                Properties = new Dictionary(StringComparer.OrdinalIgnoreCase)
+                            };
+                            items.Add(item);
+                        }
+
+                        lastReportId = reportId;
+                        var key = (string)reader["PropertyName"];
+                        var value = (string)reader["Value"];
+                        if (item.Properties.ContainsKey(key))
+                        {
+                            _logger.Info(
+                                $"Report {reportId} have value for key {key} current: {item.Properties[key]} new: {value}.");
+                        }
+                        else
+                            item.Properties.Add(key, value);
+                    }
+                }
+            }
+
+            return new GetCollectionResult
+            {
+                Items = items.ToArray()
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentForClosePage.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentForClosePage.cs
new file mode 100644
index 00000000..b05b75ee
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentForClosePage.cs
@@ -0,0 +1,33 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    internal class GetIncidentForClosePageHandler :
+        IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public GetIncidentForClosePageHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetIncidentForClosePage query)
+        {
+            using (var cmd = _uow.CreateCommand())
+            {
+                cmd.CommandText = @"select Incidents.Description, 
+(select count(*) from IncidentFeedback WHERE IncidentFeedback.IncidentId = Incidents.Id AND IncidentFeedback.EmailAddress is not null AND IncidentFeedback.EmailAddress <> '') as SubscriberCount
+FROM Incidents
+WHERE Incidents.Id = @incidentId";
+                cmd.AddParameter("incidentId", query.IncidentId);
+                return await cmd.FirstAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs
new file mode 100644
index 00000000..a6866a6f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs
@@ -0,0 +1,9 @@
+using Coderr.Server.Api.Core.Incidents.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    public class GetIncidentForClosePageResultMapper : EntityMapper
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs
new file mode 100644
index 00000000..a76f5e4d
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs
@@ -0,0 +1,298 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Incidents;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    public class GetIncidentHandler : IQueryHandler
+    {
+        private readonly IEnumerable _quickfactProviders;
+        private readonly IEnumerable _highlightedContextDataProviders;
+        private readonly IEnumerable _solutionProviders;
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private ILog _logger = LogManager.GetLogger(typeof(GetIncidentHandler));
+
+        public GetIncidentHandler(IAdoNetUnitOfWork unitOfWork,
+            IEnumerable quickfactProviders,
+            IEnumerable highlightedContextDataProviders,
+            IEnumerable solutionProviders
+            )
+        {
+            _unitOfWork = unitOfWork;
+            _quickfactProviders = quickfactProviders;
+            _highlightedContextDataProviders = highlightedContextDataProviders;
+            _solutionProviders = solutionProviders;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetIncident query)
+        {
+            _logger.Info("GetIncident step 1");
+            var sql =
+                "SELECT Incidents.*, Users.Username as AssignedTo " +
+                " FROM Incidents WITH(READUNCOMMITTED)" +
+                " LEFT JOIN Users ON (AssignedToId = Users.AccountId) " +
+                " WHERE Incidents.Id = @id";
+
+            var result = await _unitOfWork.FirstAsync(sql, new { Id = query.IncidentId });
+            _logger.Info("GetIncident step 2");
+
+            result.Tags = GetTags(query.IncidentId);
+            _logger.Info("GetIncident step 3");
+
+            var facts = new List();
+
+            var environments = GetEnvironments(query.IncidentId);
+            if (environments.Any())
+            {
+                facts.Add(new QuickFact
+                {
+                    Title = "Environments",
+                    Value = string.Join(", ", environments)
+                });
+            }
+
+            _logger.Info("GetIncident step 4");
+            await GetContextCollectionNames(result);
+            _logger.Info("GetIncident step 5");
+            await GetReportStatistics(result);
+            _logger.Info("GetIncident step 6");
+            await GetStatSummary(query, facts);
+            _logger.Info("GetIncident step 7");
+
+            var solutions = new List();
+            var suggestedSolutionContext = new SolutionProviderContext(solutions)
+            {
+                ApplicationId = result.ApplicationId,
+                Description = result.Description,
+                FullName = result.FullName,
+                IncidentId = result.Id,
+                StackTrace = result.StackTrace,
+                Tags = result.Tags
+            };
+
+            var contextData = new List();
+            var highlightedContext = new HighlightedContextDataProviderContext(contextData)
+            {
+                ApplicationId = result.ApplicationId,
+                Description = result.Description,
+                FullName = result.FullName,
+                IncidentId = result.Id,
+                StackTrace = result.StackTrace,
+                Tags = result.Tags
+            };
+            var quickFactContext = new QuickFactContext(result.ApplicationId, query.IncidentId, facts);
+            foreach (var provider in _quickfactProviders)
+            {
+                await provider.CollectAsync(quickFactContext);
+            }
+            foreach (var provider in _highlightedContextDataProviders)
+            {
+                await provider.CollectAsync(highlightedContext);
+            }
+            foreach (var provider in _solutionProviders)
+            {
+                await provider.SuggestSolutionAsync(suggestedSolutionContext);
+            }
+            _logger.Info("GetIncident step 8");
+
+            result.RelatedIncidents = await GetRelatedIncidents(query.IncidentId);
+            result.Facts = facts.Where(x => x.Value != "0" && x.Value != "").ToArray();
+            result.SuggestedSolutions = solutions.ToArray();
+            result.HighlightedContextData = contextData.ToArray();
+            return result;
+        }
+
+        //TODO : Do not mess with the similarity tables directly
+        private async Task GetContextCollectionNames(GetIncidentResult result)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"select distinct Name
+                                    from [IncidentContextCollections] WITH(READUNCOMMITTED)
+                                    where IncidentId=@incidentId";
+                cmd.AddParameter("incidentId", result.Id);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var names = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        names.Add(reader.GetString(0));
+                    }
+                    result.ContextCollections = names.ToArray();
+                }
+            }
+        }
+
+        private async Task GetRelatedIncidents(int incidentId)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"with cte (IncidentId)
+                                    AS
+                                    (
+                                      select distinct ic1.IncidentId
+                                      FROM IncidentCorrelations ic1 
+                                      JOIN IncidentCorrelations ic2 ON (ic1.CorrelationId = ic2.CorrelationId)
+                                      WHERE ic2.IncidentId = @id and ic1.IncidentId <> @id
+                                    )
+
+                                    select i.Id IncidentId, i.Description Title, i.CreatedAtUtc, i.ApplicationId, a.Name ApplicationName
+                                    FROM Incidents i
+                                    JOIN Applications a ON (a.Id = i.ApplicationId)
+                                    JOIN CTE ON (IncidentId = i.Id)";
+                cmd.AddParameter("id", incidentId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var result = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new RelatedIncident
+                        {
+                            IncidentId = reader.GetInt32(0),
+                            Title = reader.GetString(1),
+                            CreatedAtUtc = reader.GetDateTime(2),
+                            ApplicationId = reader.GetInt32(3),
+                            ApplicationName = reader.GetString(4)
+                        };
+                        result.Add(item);
+                    }
+
+                    return result.ToArray();
+                }
+            }
+        }
+
+        private async Task GetReportStatistics(GetIncidentResult result)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"select cast(ReceivedAtUtc as date) as Date, count(*) as Count
+from IncidentReports WITH(READUNCOMMITTED)
+where incidentid=@incidentId
+AND ReceivedAtUtc > @date
+group by cast(ReceivedAtUtc as date)";
+                var startDate = DateTime.Today.AddDays(-29);
+                cmd.AddParameter("date", startDate);
+                cmd.AddParameter("incidentId", result.Id);
+                var specifiedDays = await cmd.ToListAsync();
+                var curDate = startDate;
+                var values = new ReportDay[30];
+                var valuesIndexer = 0;
+                var specifiedDaysIndexer = 0;
+                while (curDate <= DateTime.Today)
+                {
+                    if (specifiedDays.Count > specifiedDaysIndexer &&
+                        specifiedDays[specifiedDaysIndexer].Date == curDate)
+                        values[valuesIndexer++] = specifiedDays[specifiedDaysIndexer++];
+                    else
+                        values[valuesIndexer++] = new ReportDay { Date = curDate };
+                    curDate = curDate.AddDays(1);
+                }
+                result.DayStatistics = values;
+            }
+        }
+
+        private async Task GetStatSummary(GetIncident query, ICollection facts)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"
+select count(distinct emailaddress) 
+from IncidentFeedback 
+where @minDate < CreatedAtUtc
+AND emailaddress is not null
+AND DATALENGTH(emailaddress) > 0
+AND IncidentId = @incidentId;
+
+select count(*) 
+from IncidentFeedback 
+where @minDate < CreatedAtUtc
+AND Description is not null
+AND DATALENGTH(Description) > 0
+AND IncidentId = @incidentId;";
+                cmd.AddParameter("incidentId", query.IncidentId);
+                cmd.AddParameter("minDate", DateTime.Today.AddDays(-90));
+
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    if (!await reader.ReadAsync())
+                        throw new InvalidOperationException("Expected to be able to read result 1.");
+
+                    facts.Add(new QuickFact
+                    {
+                        Title = "Subscribed users",
+                        Description =
+                            "Number of users that are waiting on a notification when the incident have been solved.",
+                        Value = reader.GetInt32(0).ToString()
+                    });
+
+                    await reader.NextResultAsync();
+                    if (!await reader.ReadAsync())
+                        throw new InvalidOperationException("Expected to be able to read result 2.");
+
+                    facts.Add(new QuickFact
+                    {
+                        Title = "Bug reports",
+                        Description = "Number of bug reports written by users.",
+                        Value = reader.GetInt32(0).ToString()
+                    });
+                }
+            }
+        }
+
+        private string[] GetTags(int incidentId)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"Declare @Tags AS Nvarchar(MAX);
+                                    SELECT @Tags = COALESCE(@Tags + ';', '') + TagName 
+                                    FROM IncidentTags WITH(READUNCOMMITTED)
+                                    WHERE IncidentId=@id
+                                    ORDER BY OrderNumber, TagName
+                                    SELECT @Tags";
+                cmd.AddParameter("id", incidentId);
+                using (var reader = cmd.ExecuteReader())
+                {
+                    if (!reader.Read())
+                        return new string[0];
+
+                    var value = reader[0];
+                    return value is DBNull
+                        ? new string[0]
+                        : ((string)value).Split(';');
+                }
+            }
+        }
+        private string[] GetEnvironments(int incidentId)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"Declare @Names AS Nvarchar(MAX);
+                                    SELECT @Names = COALESCE(@Names + ';', '') + Name
+                                    FROM IncidentEnvironments ie WITH(READUNCOMMITTED)
+                                    JOIN Environments ae WITH(READUNCOMMITTED) ON (ae.Id = ie.EnvironmentId) 
+                                    WHERE IncidentId=@id
+                                    ORDER BY Name
+                                    SELECT @Names";
+                cmd.AddParameter("id", incidentId);
+                using (var reader = cmd.ExecuteReader())
+                {
+                    if (!reader.Read())
+                        return new string[0];
+
+                    var value = reader[0];
+                    return value is DBNull
+                        ? new string[0]
+                        : ((string)value).Split(';');
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentResultMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentResultMapper.cs
new file mode 100644
index 00000000..62958b92
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentResultMapper.cs
@@ -0,0 +1,43 @@
+using System;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using Coderr.Server.Domain.Core.Incidents;
+using Coderr.Server.SqlServer.Tools;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    public class GetIncidentResultMapper : CrudEntityMapper
+    {
+        public GetIncidentResultMapper()
+            : base("Incidents")
+        {
+            Property(x => x.DayStatistics).Ignore();
+            Property(x => x.SuggestedSolutions).Ignore();
+            Property(x => x.Facts).Ignore();
+            Property(x => x.HighlightedContextData).Ignore();
+            Property(x => x.Tags).Ignore();
+            Property(x => x.ContextCollections).Ignore();
+            Property(x => x.IncidentState)
+                .ColumnName("State");
+            Property(x => x.AssignedToId)
+                .ToPropertyValue(x => x is DBNull ? (int?)null : (int)x);
+
+            Property(x => x.Solution)
+                .ToPropertyValue(x => EntitySerializer.Deserialize(x)?.Description);
+
+            Property(x => x.LastReportReceivedAtUtc)
+                .ToPropertyValue(DbConverters.ToEntityDate)
+                .ColumnName("LastReportAtUtc");
+
+            Property(x => x.IsSolved).Ignore();
+            Property(x => x.IsIgnored).Ignore();
+
+            Property(x => x.IsSolutionShared)
+                .ToPropertyValue(DbConverters.BoolFromByteArray);
+
+            Property(x => x.RelatedIncidents)
+                .Ignore();
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs
similarity index 81%
rename from src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs
index 3d73e642..8dd9a1a2 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetIncidentStatisticsHandler.cs
@@ -1,109 +1,108 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using OneTrueError.Api.Core.Incidents.Queries;
-
-namespace OneTrueError.SqlServer.Core.Incidents.Queries
-{
-    [Component]
-    internal class GetIncidentStatisticsHandler : IQueryHandler
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public GetIncidentStatisticsHandler(IAdoNetUnitOfWork unitOfWork)
-        {
-            if (unitOfWork == null) throw new ArgumentNullException("unitOfWork");
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task ExecuteAsync(GetIncidentStatistics query)
-        {
-            if (query.NumberOfDays == 1)
-                return await GetTodaysOverviewAsync(query);
-
-            var result = new GetIncidentStatisticsResult
-            {
-                Labels = new string[query.NumberOfDays],
-                Values = new int[query.NumberOfDays]
-            };
-
-            var startDate = DateTime.Today.AddDays(-query.NumberOfDays + 1);
-            for (var i = 0; i < query.NumberOfDays; i++)
-            {
-                result.Values[i] = 0;
-                result.Labels[i] = startDate.AddDays(i).ToShortDateString();
-            }
-
-            using (var cmd = _unitOfWork.CreateDbCommand())
-            {
-                cmd.CommandText = @"select cast(createdatutc as date) as Date, count(*) as Count
-from errorreports
-where incidentid=@id
-AND CreatedAtUtc > @date
-group by cast(createdatutc as date)";
-                cmd.AddParameter("id", query.IncidentId);
-                cmd.AddParameter("date", DateTime.Today.AddDays(0 - query.NumberOfDays));
-                using (var reader = await cmd.ExecuteReaderAsync())
-                {
-                    while (await reader.ReadAsync())
-                    {
-                        var index = reader.GetDateTime(0).Subtract(startDate).Days;
-                        result.Values[index] = reader.GetInt32(1);
-                    }
-                }
-            }
-
-            return result;
-        }
-
-        private async Task GetTodaysOverviewAsync(GetIncidentStatistics query)
-        {
-            var result = new GetIncidentStatisticsResult
-            {
-                Labels = new string[24],
-                Values = new int[24]
-            };
-            var values = new Dictionary();
-            var startDate = DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23);
-            for (var i = 0; i < 24; i++)
-            {
-                result.Values[i] = 0;
-                result.Labels[i] = startDate.AddHours(i).ToString("HH:mm");
-                values[startDate.AddHours(i)] = 0;
-            }
-
-            using (var cmd = _unitOfWork.CreateDbCommand())
-            {
-                cmd.CommandText = @"SELECT DATEPART(HOUR, ErrorReports.CreatedAtUtc), cast(count(Id) as int)
-FROM ErrorReports
-WHERE ErrorReports.CreatedAtUtc > @minDate
-AND IncidentId = @incidentId
-GROUP BY DATEPART(HOUR, ErrorReports.CreatedAtUtc);";
-
-
-                cmd.AddParameter("incidentId", query.IncidentId);
-                cmd.AddParameter("minDate", startDate);
-                using (var reader = await cmd.ExecuteReaderAsync())
-                {
-                    while (await reader.ReadAsync())
-                    {
-                        var todayWithHour = DateTime.Today.AddHours(DateTime.Now.Hour);
-                        var hour = reader.GetInt32(0);
-                        var date = hour < todayWithHour.Hour
-                            ? DateTime.Today.AddHours(hour)
-                            : DateTime.Today.AddDays(-1).AddHours(hour);
-                        values[date] = reader.GetInt32(1);
-                    }
-                }
-            }
-
-            result.Values = values.Values.ToArray();
-
-            return result;
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Incidents.Queries;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    internal class GetIncidentStatisticsHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetIncidentStatisticsHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            if (unitOfWork == null) throw new ArgumentNullException("unitOfWork");
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetIncidentStatistics query)
+        {
+            if (query.NumberOfDays == 1)
+                return await GetTodaysOverviewAsync(query);
+
+            var result = new GetIncidentStatisticsResult
+            {
+                Labels = new string[query.NumberOfDays],
+                Values = new int[query.NumberOfDays]
+            };
+
+            var startDate = DateTime.Today.AddDays(-query.NumberOfDays + 1);
+            for (var i = 0; i < query.NumberOfDays; i++)
+            {
+                result.Values[i] = 0;
+                result.Labels[i] = startDate.AddDays(i).ToShortDateString();
+            }
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"select cast(ReceivedAtUtc as date) as Date, count(*) as Count
+from IncidentReports
+where incidentid=@id
+AND ReceivedAtUtc > @date
+group by cast(ReceivedAtUtc as date)";
+                cmd.AddParameter("id", query.IncidentId);
+                cmd.AddParameter("date", DateTime.Today.AddDays(0 - query.NumberOfDays));
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var index = reader.GetDateTime(0).Subtract(startDate).Days;
+                        result.Values[index] = reader.GetInt32(1);
+                    }
+                }
+            }
+
+            return result;
+        }
+
+        private async Task GetTodaysOverviewAsync(GetIncidentStatistics query)
+        {
+            var result = new GetIncidentStatisticsResult
+            {
+                Labels = new string[24],
+                Values = new int[24]
+            };
+            var values = new Dictionary();
+            var startDate = DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23);
+            for (var i = 0; i < 24; i++)
+            {
+                result.Values[i] = 0;
+                result.Labels[i] = startDate.AddHours(i).ToString("HH:mm");
+                values[startDate.AddHours(i)] = 0;
+            }
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"SELECT DATEPART(HOUR, IncidentReports.ReceivedAtUtc), cast(count(Id) as int)
+FROM IncidentReports WITH (READUNCOMMITTED)
+WHERE IncidentReports.ReceivedAtUtc > @minDate
+AND IncidentId = @incidentId
+GROUP BY DATEPART(HOUR, IncidentReports.ReceivedAtUtc);";
+
+
+                cmd.AddParameter("incidentId", query.IncidentId);
+                cmd.AddParameter("minDate", startDate);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var todayWithHour = DateTime.Today.AddHours(DateTime.Now.Hour);
+                        var hour = reader.GetInt32(0);
+                        var date = hour < todayWithHour.Hour
+                            ? DateTime.Today.AddHours(hour)
+                            : DateTime.Today.AddDays(-1).AddHours(hour);
+                        values[date] = reader.GetInt32(1);
+                    }
+                }
+            }
+
+            result.Values = values.Values.ToArray();
+
+            return result;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetReportListHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetReportListHandler.cs
similarity index 79%
rename from src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetReportListHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetReportListHandler.cs
index 2fc5c085..a78ce650 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetReportListHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetReportListHandler.cs
@@ -1,53 +1,57 @@
-using System.Linq;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.Api.Core.Reports.Queries;
-using OneTrueError.SqlServer.Tools;
-
-namespace OneTrueError.SqlServer.Core.Incidents.Queries
-{
-    [Component]
-    internal class GetReportListHandler : IQueryHandler
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public GetReportListHandler(IAdoNetUnitOfWork unitOfWork)
-        {
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task ExecuteAsync(GetReportList query)
-        {
-            using (var cmd = _unitOfWork.CreateCommand())
-            {
-                var totalCount = 0;
-                cmd.AddParameter("incidentId", query.IncidentId);
-                if (query.PageNumber > 0)
-                {
-                    cmd.CommandText = "SELECT cast(count(Id) as int) FROM ErrorReports WHERE IncidentId = @incidentId";
-                    totalCount = (int) cmd.ExecuteScalar();
-
-                    cmd.CommandText =
-                        "SELECT Id, Title, CreatedAtUtc, RemoteAddress, Exception FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY Id DESC";
-
-                    cmd.Paging(query.PageNumber, query.PageSize);
-                }
-                else
-                {
-                    cmd.CommandText =
-                        "SELECT Id, Title, CreatedAtUtc, RemoteAddress, Exception FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY Id DESC";
-                }
-                var items = await cmd.ToListAsync();
-                return new GetReportListResult(items.ToArray())
-                {
-                    PageNumber = query.PageNumber,
-                    PageSize = query.PageSize,
-                    TotalCount = totalCount
-                };
-            }
-        }
-    }
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Reports.Queries;
+using Coderr.Server.SqlServer.Tools;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    internal class GetReportListHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetReportListHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetReportList query)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                var totalCount = 0;
+                cmd.AddParameter("incidentId", query.IncidentId);
+                if (query.PageNumber > 0)
+                {
+                    if (query.PageSize == 0)
+                    {
+                        query.PageSize = 10;
+                    }
+
+                    cmd.CommandText = "SELECT cast(count(Id) as int) FROM ErrorReports WHERE IncidentId = @incidentId";
+                    totalCount = (int) cmd.ExecuteScalar();
+
+                    cmd.CommandText =
+                        "SELECT Id, Title, CreatedAtUtc, RemoteAddress, Exception FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY Id DESC";
+
+                    cmd.Paging(query.PageNumber, query.PageSize);
+                }
+                else
+                {
+                    cmd.CommandText =
+                        "SELECT Id, Title, CreatedAtUtc, RemoteAddress, Exception FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY Id DESC";
+                }
+                var items = await cmd.ToListAsync();
+                return new GetReportListResult(items.ToArray())
+                {
+                    PageNumber = query.PageNumber,
+                    PageSize = query.PageSize,
+                    TotalCount = totalCount
+                };
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetReportListResultItemMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetReportListResultItemMapper.cs
new file mode 100644
index 00000000..5cb7edd6
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/Queries/GetReportListResultItemMapper.cs
@@ -0,0 +1,13 @@
+using Coderr.Server.Api.Core.Reports.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents.Queries
+{
+    public class GetReportListResultItemMapper : EntityMapper
+    {
+        public GetReportListResultItemMapper()
+        {
+            Property(x => x.Message).ColumnName("Title");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Incidents/ReportDayMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Incidents/ReportDayMapper.cs
new file mode 100644
index 00000000..249bfa85
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Incidents/ReportDayMapper.cs
@@ -0,0 +1,9 @@
+using Coderr.Server.Api.Core.Incidents.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Incidents
+{
+    internal class ReportDayMapper : EntityMapper
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Invitations/GetInvitationByKeyHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Invitations/GetInvitationByKeyHandler.cs
new file mode 100644
index 00000000..13a3df06
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Invitations/GetInvitationByKeyHandler.cs
@@ -0,0 +1,28 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Invitations.Queries;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Core.Invitations
+{
+    internal class GetInvitationByKeyHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetInvitationByKeyHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetInvitationByKey query)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT email FROM Invitations WHERE InvitationKey = @id";
+                cmd.AddParameter("id", query.InvitationKey);
+                return new GetInvitationByKeyResult {EmailAddress = (string) await cmd.ExecuteScalarAsync()};
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Invitations/InvitationMapping.cs b/src/Server/Coderr.Server.SqlServer/Core/Invitations/InvitationMapping.cs
new file mode 100644
index 00000000..f0fed1e6
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Invitations/InvitationMapping.cs
@@ -0,0 +1,20 @@
+using System.Collections.Generic;
+using Coderr.Server.App.Core.Invitations;
+using Coderr.Server.SqlServer.Tools;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Invitations
+{
+    public class InvitationMapping : CrudEntityMapper
+    {
+        public InvitationMapping()
+            : base("Invitations")
+        {
+            Property(x => x.Id).PrimaryKey(true);
+            Property(x => x.EmailToInvitedUser).ColumnName("Email");
+            Property(x => x.Invitations)
+                .ToColumnValue(EntitySerializer.Serialize)
+                .ToPropertyValue(x => EntitySerializer.Deserialize>((string) x));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/Invitations/InvitationRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Invitations/InvitationRepository.cs
similarity index 87%
rename from src/Server/OneTrueError.SqlServer/Core/Invitations/InvitationRepository.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Invitations/InvitationRepository.cs
index d2a50807..63ec8584 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Invitations/InvitationRepository.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Invitations/InvitationRepository.cs
@@ -1,50 +1,51 @@
-using System.Threading.Tasks;
-using Griffin.Container;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.App.Core.Invitations;
-
-namespace OneTrueError.SqlServer.Core.Invitations
-{
-    [Component]
-    internal class InvitationRepository : IInvitationRepository
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public InvitationRepository(IAdoNetUnitOfWork unitOfWork)
-        {
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task DeleteAsync(string invitationKey)
-        {
-            using (var cmd = _unitOfWork.CreateDbCommand())
-            {
-                cmd.CommandText = "DELETE FROM Invitations WHERE InvitationKey = @key";
-                cmd.AddParameter("key", invitationKey);
-                await cmd.ExecuteNonQueryAsync();
-            }
-        }
-
-
-        public async Task FindByEmailAsync(string email)
-        {
-            return await _unitOfWork.FirstOrDefaultAsync(new {EmailToInvitedUser = email});
-        }
-
-        public async Task CreateAsync(Invitation invitation)
-        {
-            await _unitOfWork.InsertAsync(invitation);
-        }
-
-        public async Task UpdateAsync(Invitation invitation)
-        {
-            await _unitOfWork.UpdateAsync(invitation);
-        }
-
-        public async Task GetByInvitationKeyAsync(string invitationKey)
-        {
-            return await _unitOfWork.FirstOrDefaultAsync(new {InvitationKey = invitationKey});
-        }
-    }
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.App.Core.Invitations;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Invitations
+{
+    [ContainerService]
+    internal class InvitationRepository : IInvitationRepository
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public InvitationRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task DeleteAsync(string invitationKey)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = "DELETE FROM Invitations WHERE InvitationKey = @key";
+                cmd.AddParameter("key", invitationKey);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+
+        public async Task FindByEmailAsync(string email)
+        {
+            return await _unitOfWork.FirstOrDefaultAsync(new {EmailToInvitedUser = email});
+        }
+
+        public async Task CreateAsync(Invitation invitation)
+        {
+            await _unitOfWork.InsertAsync(invitation);
+        }
+
+        public async Task UpdateAsync(Invitation invitation)
+        {
+            await _unitOfWork.UpdateAsync(invitation);
+        }
+
+        public async Task GetByInvitationKeyAsync(string invitationKey)
+        {
+            return await _unitOfWork.FirstOrDefaultAsync(new {InvitationKey = invitationKey});
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Notifications/BrowserSubscriptionMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Notifications/BrowserSubscriptionMapper.cs
new file mode 100644
index 00000000..ee0fc686
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Notifications/BrowserSubscriptionMapper.cs
@@ -0,0 +1,14 @@
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Notifications
+{
+    internal class BrowserSubscriptionMapper : CrudEntityMapper
+    {
+        public BrowserSubscriptionMapper() : base("NotificationsBrowser")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Notifications/NotificationRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Notifications/NotificationRepository.cs
new file mode 100644
index 00000000..5d99e4db
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Notifications/NotificationRepository.cs
@@ -0,0 +1,142 @@
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.App.Core.Notifications;
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Notifications
+{
+    [ContainerService]
+    public class NotificationRepository : INotificationsRepository, IUserNotificationsRepository
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public NotificationRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task TryGetAsync(int accountId, int applicationId)
+        {
+            if (applicationId == 0)
+                applicationId = -1;
+            return await _unitOfWork.FirstOrDefaultAsync(
+                new {AccountId = accountId, ApplicationId = applicationId});
+        }
+
+        public async Task UpdateAsync(UserNotificationSettings notificationSettings)
+        {
+            if (notificationSettings.ApplicationId == 0)
+                notificationSettings.ApplicationId = -1;
+            await _unitOfWork.UpdateAsync(notificationSettings);
+        }
+
+        public async Task> GetSubscriptions(int accountId)
+        {
+            return await _unitOfWork.ToListAsync(
+                "SELECT * FROM NotificationsBrowser WHERE AccountId = @id",
+                new {id = accountId});
+        }
+
+        public async Task Delete(BrowserSubscription subscription)
+        {
+            await _unitOfWork.DeleteAsync(subscription);
+        }
+
+        public async Task Save(BrowserSubscription message)
+        {
+            // Only store one per user, not matter how many browsers they register for.
+            // Earlier, we identified them using "message.Endpoint" too, but EPs seem to fail
+            // when doing so, so let's just keep one (and overwrite the others).
+
+            var existing =
+                await _unitOfWork.FirstOrDefaultAsync(new {message.AccountId});
+
+            if (existing != null)
+            {
+                existing.PublicKey = message.PublicKey;
+                existing.AuthenticationSecret = message.AuthenticationSecret;
+                existing.ExpiresAtUtc = message.ExpiresAtUtc;
+                existing.Endpoint = message.Endpoint;
+                await _unitOfWork.UpdateAsync(existing);
+            }
+            else
+            {
+                await _unitOfWork.InsertAsync(message);
+            }
+        }
+
+        public async Task DeleteBrowserSubscription(int accountId, string endpoint)
+        {
+            await _unitOfWork.DeleteAsync(new {AccountId = accountId, Endpoint = endpoint});
+        }
+
+        public async Task CreateAsync(UserNotificationSettings notificationSettings)
+        {
+            if (notificationSettings.ApplicationId == 0)
+                notificationSettings.ApplicationId = -1;
+            await _unitOfWork.InsertAsync(notificationSettings);
+        }
+
+        public async Task> GetAllAsync(int applicationId)
+        {
+            var sql =
+                @"SELECT * 
+                    FROM UserNotificationSettings 
+                    WHERE ApplicationId = @1 
+                        OR ApplicationId = -1 
+                    ORDER By AccountId, ApplicationId DESC";
+            var settings = await _unitOfWork.ToListAsync(sql, applicationId);
+            var dict = new Dictionary();
+            foreach (var setting in settings)
+            {
+                if (!dict.TryGetValue(setting.AccountId, out var appSettings))
+                {
+                    dict[setting.AccountId] = setting;
+                    continue;
+                }
+
+                // We got the most specific settings thanks to the ASC sort.
+                // now check if the app settings have "use global"
+
+                if (appSettings.ApplicationSpike == NotificationState.UseGlobalSetting)
+                    appSettings.ApplicationSpike = setting.ApplicationSpike;
+
+                if (appSettings.NewIncident == NotificationState.UseGlobalSetting)
+                    appSettings.NewIncident = setting.NewIncident;
+
+                if (appSettings.ReopenedIncident == NotificationState.UseGlobalSetting)
+                    appSettings.ReopenedIncident = setting.ReopenedIncident;
+
+                if (appSettings.UserFeedback == NotificationState.UseGlobalSetting)
+                    appSettings.UserFeedback = setting.UserFeedback;
+
+                if (appSettings.WeeklySummary == NotificationState.UseGlobalSetting)
+                    appSettings.WeeklySummary = setting.WeeklySummary;
+
+                if (appSettings.ReopenedIncident == NotificationState.UseGlobalSetting)
+                    appSettings.ReopenedIncident = setting.ReopenedIncident;
+            }
+
+            return dict.Values.ToList();
+        }
+
+        public async Task ExistsAsync(int accountId, int applicationId)
+        {
+            if (applicationId == 0)
+                applicationId = -1;
+            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "SELECT TOP 1 AccountId FROM UserNotificationSettings WHERE AccountId = @id AND ApplicationId = @appId";
+                cmd.AddParameter("id", accountId);
+                cmd.AddParameter("appId", applicationId);
+                return await cmd.ExecuteScalarAsync() != null;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Notifications/NotificationService.cs b/src/Server/Coderr.Server.SqlServer/Core/Notifications/NotificationService.cs
new file mode 100644
index 00000000..76284e6a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Notifications/NotificationService.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Client;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Coderr.Server.ReportAnalyzer.UserNotifications;
+using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos;
+using log4net;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.SqlServer.Core.Notifications
+{
+    // Must be here so that it can be used from both queues.
+    /// 
+    /// Implementation of .
+    /// 
+    [ContainerService]
+    public class NotificationService : INotificationService
+    {
+        private readonly IWebPushClient _client;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(NotificationService));
+        private readonly IUserNotificationsRepository _repository;
+
+        public NotificationService(IUserNotificationsRepository repository, IWebPushClient client)
+        {
+            _repository = repository;
+            _client = client;
+        }
+
+        public async Task SendBrowserNotification(int accountId, Notification notification)
+        {
+            if (notification == null) throw new ArgumentNullException(nameof(notification));
+            if (accountId <= 0) throw new ArgumentOutOfRangeException(nameof(accountId));
+
+            var subscriptions = await _repository.GetSubscriptions(accountId);
+            foreach (var subscription in subscriptions)
+            {
+                try
+                {
+                    _logger.Info("sending " + JsonConvert.SerializeObject(notification) + " to " + subscription.AccountId);
+                    await _client.SendNotification(subscription, notification);
+                }
+                catch (InvalidSubscriptionException e)
+                {
+                    Err.Report(e, new {accountId, notification, subscription });
+                    _logger.Error("Failed to send notification to " + subscription.AccountId, e);
+                    await _repository.Delete(subscription);
+                }
+                catch (Exception e)
+                {
+                    Err.Report(e, new { accountId, notification, subscription });
+                    _logger.Error("Failed to send notification to " + subscription.AccountId, e);
+                }
+            }
+        }
+   }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/Notifications/UserNotificationSettingsMap.cs b/src/Server/Coderr.Server.SqlServer/Core/Notifications/UserNotificationSettingsMap.cs
similarity index 85%
rename from src/Server/OneTrueError.SqlServer/Core/Notifications/UserNotificationSettingsMap.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Notifications/UserNotificationSettingsMap.cs
index 02f911e9..cf0232f6 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Notifications/UserNotificationSettingsMap.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Notifications/UserNotificationSettingsMap.cs
@@ -1,54 +1,57 @@
-using System;
-using Griffin.Data.Mapper;
-using OneTrueError.App.Core.Notifications;
-
-namespace OneTrueError.SqlServer.Core.Notifications
-{
-    internal class UserNotificationSettingsMap : CrudEntityMapper
-    {
-        public UserNotificationSettingsMap()
-            : base("UserNotificationSettings")
-        {
-            Property(x => x.AccountId).ColumnName("AccountId").PrimaryKey();
-            Property(x => x.ApplicationId).PrimaryKey();
-            Property(x => x.NewIncident)
-                .ToColumnValue(StringToEnum)
-                .ToPropertyValue(EnumToString);
-            Property(x => x.ReopenedIncident)
-                .ToColumnValue(StringToEnum)
-                .ToPropertyValue(EnumToString);
-            Property(x => x.NewReport)
-                .ToColumnValue(StringToEnum)
-                .ToPropertyValue(EnumToString);
-            Property(x => x.UserFeedback)
-                .ToColumnValue(StringToEnum)
-                .ToPropertyValue(EnumToString);
-            Property(x => x.ApplicationSpike)
-                .ToColumnValue(StringToEnum)
-                .ToPropertyValue(EnumToString);
-            Property(x => x.WeeklySummary)
-                .ToColumnValue(StringToEnum)
-                .ToPropertyValue(EnumToString);
-        }
-
-        public static object StringToEnum(TEnum notificationState)
-        {
-            return notificationState.ToString();
-        }
-
-        private TEnum EnumToString(object arg) where TEnum : struct
-        {
-            if (arg is DBNull)
-                return default(TEnum);
-
-            TEnum en;
-            if (!Enum.TryParse(arg.ToString(), true, out en))
-            {
-                throw new MappingException(typeof(UserNotificationSettings),
-                    "Failed to convert '" + arg + "' to '" + typeof(TEnum).FullName + "'.");
-            }
-
-            return en;
-        }
-    }
+using System;
+using Coderr.Server.Domain.Modules.UserNotifications;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Notifications
+{
+    internal class UserNotificationSettingsMap : CrudEntityMapper
+    {
+        public UserNotificationSettingsMap()
+            : base("UserNotificationSettings")
+        {
+            Property(x => x.AccountId).ColumnName("AccountId").PrimaryKey();
+            Property(x => x.ApplicationId).PrimaryKey();
+            Property(x => x.NewIncident)
+                .ToColumnValue(StringToEnum)
+                .ToPropertyValue(EnumToString);
+            Property(x => x.CriticalIncident)
+                .ToColumnValue(StringToEnum)
+                .ToPropertyValue(EnumToString);
+            Property(x => x.ImportantIncident)
+                .ToColumnValue(StringToEnum)
+                .ToPropertyValue(EnumToString);
+            Property(x => x.ReopenedIncident)
+                .ToColumnValue(StringToEnum)
+                .ToPropertyValue(EnumToString);
+            Property(x => x.UserFeedback)
+                .ToColumnValue(StringToEnum)
+                .ToPropertyValue(EnumToString);
+            Property(x => x.ApplicationSpike)
+                .ToColumnValue(StringToEnum)
+                .ToPropertyValue(EnumToString);
+            Property(x => x.WeeklySummary)
+                .ToColumnValue(StringToEnum)
+                .ToPropertyValue(EnumToString);
+        }
+
+        public static object StringToEnum(TEnum notificationState)
+        {
+            return notificationState.ToString();
+        }
+
+        private TEnum EnumToString(object arg) where TEnum : struct
+        {
+            if (arg is DBNull)
+                return default(TEnum);
+
+            TEnum en;
+            if (!Enum.TryParse(arg.ToString(), true, out en))
+            {
+                throw new MappingException(typeof(UserNotificationSettings),
+                    "Failed to convert '" + arg + "' to '" + typeof(TEnum).FullName + "'.");
+            }
+
+            return en;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Reports/ErrorReportDtoMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Reports/ErrorReportDtoMapper.cs
new file mode 100644
index 00000000..6fdb7a9c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Reports/ErrorReportDtoMapper.cs
@@ -0,0 +1,28 @@
+using Coderr.Server.Api.Core.Reports;
+using Coderr.Server.SqlServer.Tools;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Reports
+{
+    public class ErrorReportDtoMapper : CrudEntityMapper
+    {
+        public ErrorReportDtoMapper()
+            : base("ErrorReports")
+        {
+            //Property(x => x.ContextCollections)
+            //    .ColumnName("ContextInfo")
+            //    .ToColumnValue(EntitySerializer.Serialize)
+            //    .ToPropertyValue(colValue => EntitySerializer.Deserialize((string) colValue));
+            Property(x => x.ContextCollections).Ignore();
+
+            Property(x => x.ReportVersion).Ignore();
+
+            Property(x => x.Exception)
+                .ToPropertyValue(EntitySerializer.Deserialize)
+                .ToColumnValue(EntitySerializer.Serialize);
+
+            Property(x => x.ReportId)
+                .ColumnName("ErrorId");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Reports/ErrorReportRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Reports/ErrorReportRepository.cs
new file mode 100644
index 00000000..815ca860
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Reports/ErrorReportRepository.cs
@@ -0,0 +1,157 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Api.Core.Reports;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Core.Reports
+{
+    [ContainerService]
+    internal class ErrorReportRepository : IReportsRepository
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+        private ILog _logger = LogManager.GetLogger(typeof(ErrorReportRepository));
+
+        public ErrorReportRepository(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow ?? throw new ArgumentNullException(nameof(uow));
+        }
+
+        public async Task GetAsync(int id)
+        {
+            ErrorReportEntity report;
+
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    "SELECT * FROM ErrorReports With (ReadUncommitted) WHERE Id = @id";
+
+                cmd.AddParameter("id", id);
+                report = await cmd.FirstAsync();
+            }
+
+            var collections = new List();
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"SELECT Name, PropertyName, Value 
+                        FROM ErrorReportCollectionProperties WITH(ReadUncommitted)
+                        WHERE ReportId = @id 
+                        ORDER BY Name";
+
+                cmd.AddParameter("id", id);
+
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    string previousCollectionName = null;
+                    var properties = new Dictionary(StringComparer.OrdinalIgnoreCase);
+                    string currentCollectionName = null;
+                    while (await reader.ReadAsync())
+                    {
+                        currentCollectionName = reader.GetString(0);
+
+                        // We always want to add the context when the last property have been found
+                        // so that all props are included.
+                        if (previousCollectionName == null)
+                            previousCollectionName = currentCollectionName;
+
+                        if (previousCollectionName != currentCollectionName)
+                        {
+                            var collection = new ErrorReportContextCollection(previousCollectionName ?? currentCollectionName, properties);
+                            collections.Add(collection);
+                            properties = new Dictionary(StringComparer.OrdinalIgnoreCase);
+                            previousCollectionName = currentCollectionName;
+                            report.Add(collection);
+                        }
+
+                        properties[reader.GetString(1)] = reader.GetString(2);
+                    }
+
+                    // When the last property is in a new collection
+                    if (currentCollectionName != null && collections.All(x => x.Name != currentCollectionName))
+                    {
+                        var collection = new ErrorReportContextCollection(previousCollectionName, properties);
+                        collections.Add(collection);
+                        report.Add(collection);
+                    }
+
+                }
+            }
+
+            _logger.Debug("Getting4..");
+            return report;
+        }
+
+        public async Task FindByErrorIdAsync(string errorId)
+        {
+            using (var cmd = (DbCommand)_uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"SELECT ir.Id, ir.IncidentId, i.ApplicationId, ir.ReceivedAtUtc, ir.ErrorId
+                        FROM IncidentReports ir WITH (READUNCOMMITTED)
+                        JOIN Incidents i  WITH (READUNCOMMITTED) ON (i.Id = ir.IncidentId)
+                        WHERE ErrorId = @id";
+
+                cmd.AddParameter("id", errorId);
+                return await cmd.FirstOrDefaultAsync();
+            }
+        }
+
+        //public async Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize)
+        //{
+        //    using (var cmd = (DbCommand)_uow.CreateCommand())
+        //    {
+        //        cmd.AddParameter("incidentId", incidentId);
+        //        long totalRows = 0;
+        //        if (pageNumber > 0)
+        //        {
+        //            cmd.CommandText =
+        //                "SELECT count(*) FROM ErrorReports WHERE IncidentId = @incidentId";
+        //            totalRows = (int)await cmd.ExecuteScalarAsync();
+        //        }
+
+        //        cmd.CommandText =
+        //            "SELECT * FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY CreatedAtUtc DESC";
+        //        if (pageNumber > 0)
+        //        {
+        //            var offset = (pageNumber - 1) * pageSize;
+        //            cmd.CommandText += string.Format(@" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize);
+        //        }
+
+        //        //cmd.AddParameter("incidentId", incidentId);
+        //        var list = await cmd.ToListAsync();
+        //        return new PagedReports
+        //        {
+        //            TotalCount = (int)totalRows,
+        //            Reports = (IReadOnlyList)list
+        //        };
+        //    }
+        //}
+
+        [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
+        public IEnumerable GetAll(int[] ids)
+        {
+            //TODO: Remove SQL injection vulnerability
+
+            using (var cmd = _uow.CreateCommand())
+            {
+                var idString = string.Join(",", ids.Select(x => "'" + x + "'"));
+
+                cmd.CommandText =
+                    string.Format(
+                        "SELECT * FROM ErrorReports WHERE Id IN ({0})",
+                        idString);
+
+                return cmd.ToList().ToList();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Reports/ReportMappingMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Reports/ReportMappingMapper.cs
new file mode 100644
index 00000000..323da8fa
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Reports/ReportMappingMapper.cs
@@ -0,0 +1,16 @@
+using Coderr.Server.Domain.Core.ErrorReports;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Reports
+{
+    class ReportMappingMapper : CrudEntityMapper
+    {
+        public ReportMappingMapper() : base("IncidentReports")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+            Property(x => x.ApplicationId)
+                .NotForCrud();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Settings/AccountSetting.cs b/src/Server/Coderr.Server.SqlServer/Core/Settings/AccountSetting.cs
new file mode 100644
index 00000000..db888d20
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Settings/AccountSetting.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Coderr.Server.SqlServer.Core.Settings
+{
+    public class AccountSetting
+    {
+        public int AccountId { get; set; }
+        public string Name { get; set; }
+        public string Value { get; set; }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Settings/AccountSettingMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Settings/AccountSettingMapper.cs
new file mode 100644
index 00000000..c3b0e53d
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Settings/AccountSettingMapper.cs
@@ -0,0 +1,13 @@
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Settings
+{
+    public class AccountSettingMapper : CrudEntityMapper
+    {
+        public AccountSettingMapper():base("UserSettings")
+        {
+            Property(x => x.AccountId).PrimaryKey(false);
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Settings/GetAccountSettingHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Settings/GetAccountSettingHandler.cs
new file mode 100644
index 00000000..2919e65b
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Settings/GetAccountSettingHandler.cs
@@ -0,0 +1,25 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Settings.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Settings
+{
+    class GetAccountSettingHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public GetAccountSettingHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetAccountSetting query)
+        {
+            var setting = await _uow.FirstOrDefaultAsync("AccountId = @AccountId AND Name = @Name", new { query.AccountId, query.Name });
+            return new GetAccountSettingResult { Value = setting?.Value };
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Settings/GetAccountSettingsHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Settings/GetAccountSettingsHandler.cs
new file mode 100644
index 00000000..b8970c6f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Settings/GetAccountSettingsHandler.cs
@@ -0,0 +1,25 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Settings.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Settings
+{
+    class GetAccountSettingsHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public GetAccountSettingsHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetAccountSettings query)
+        {
+            var items = await _uow.ToListAsync("AccountId = @AccountId", new { AccountId = query.AccountId });
+            return new GetAccountSettingsResult { Settings = items.ToDictionary(x => x.Name, x => x.Value) };
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Settings/SaveAccountSettingHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Settings/SaveAccountSettingHandler.cs
new file mode 100644
index 00000000..c872948c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Settings/SaveAccountSettingHandler.cs
@@ -0,0 +1,37 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Settings.Commands;
+using Coderr.Server.Api.Core.Settings.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Settings
+{
+    internal class SaveAccountSettingHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public SaveAccountSettingHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, SaveAccountSetting message)
+        {
+            var setting =
+                await _unitOfWork.FirstOrDefaultAsync("AccountId  = @AccountId AND Name = @Name", message);
+            if (setting != null)
+            {
+                setting.Value = message.Value;
+                await _unitOfWork.UpdateAsync(setting);
+            }
+            else
+            {
+                await _unitOfWork.InsertAsync(new AccountSetting
+                {
+                    AccountId = message.AccountId, Name = message.Name, Value = message.Value
+                });
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Settings/SaveAccountSettingsHandler.cs b/src/Server/Coderr.Server.SqlServer/Core/Settings/SaveAccountSettingsHandler.cs
new file mode 100644
index 00000000..78e805db
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Settings/SaveAccountSettingsHandler.cs
@@ -0,0 +1,47 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Core.Settings.Commands;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Settings
+{
+    class SaveAccountSettingsHandler : IMessageHandler
+    {
+        private IAdoNetUnitOfWork _unitOfWork;
+
+        public SaveAccountSettingsHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, SaveAccountSettings message)
+        {
+            foreach (var setting in message.Settings)
+            {
+                await SaveSetting(message.AccountId, setting.Key, setting.Value);
+            }
+        }
+
+        private async Task SaveSetting(int accountId, string name, string value)
+        {
+            var entity =
+                await _unitOfWork.FirstOrDefaultAsync("AccountId  = @AccountId AND Name = @Name",
+                    new { accountId, Name = name });
+            if (entity != null)
+            {
+                entity.Value = value;
+                await _unitOfWork.UpdateAsync(entity);
+            }
+            else
+            {
+                await _unitOfWork.InsertAsync(new AccountSetting
+                {
+                    AccountId = accountId,
+                    Name = name,
+                    Value = value
+                });
+            }
+        }
+    }
+}
diff --git a/src/Server/OneTrueError.SqlServer/Core/Users/ApplicationTeamMemberMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Users/ApplicationTeamMemberMapper.cs
similarity index 87%
rename from src/Server/OneTrueError.SqlServer/Core/Users/ApplicationTeamMemberMapper.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Users/ApplicationTeamMemberMapper.cs
index 7acebc47..a0b76120 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Users/ApplicationTeamMemberMapper.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Users/ApplicationTeamMemberMapper.cs
@@ -1,26 +1,26 @@
-using System;
-using Griffin.Data.Mapper;
-using OneTrueError.App.Core.Users;
-
-namespace OneTrueError.SqlServer.Core.Users
-{
-    public class ApplicationTeamMemberMapper : CrudEntityMapper
-    {
-        public ApplicationTeamMemberMapper() : base("ApplicationMembers")
-        {
-            Property(x => x.Id)
-                .PrimaryKey(true);
-
-            Property(x => x.AccountId)
-                .ToColumnValue(x => x == 0 ? (object)DBNull.Value : x)
-                .ToPropertyValue(x => x is DBNull ? 0 : (int)x);
-
-            Property(x => x.UserName)
-                .NotForCrud();
-
-            Property(x => x.Roles)
-                .ToColumnValue(x => string.Join(",", x))
-                .ToPropertyValue(x => ((string)x).Split(','));
-        }
-    }
+using System;
+using Coderr.Server.Domain.Core.Applications;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Users
+{
+    public class ApplicationTeamMemberMapper : CrudEntityMapper
+    {
+        public ApplicationTeamMemberMapper() : base("ApplicationMembers")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+
+            Property(x => x.AccountId)
+                .ToColumnValue(x => x == 0 ? (object)DBNull.Value : x)
+                .ToPropertyValue(x => x is DBNull ? 0 : (int)x);
+
+            Property(x => x.UserName)
+                .NotForCrud();
+
+            Property(x => x.Roles)
+                .ToColumnValue(x => string.Join(",", x))
+                .ToPropertyValue(x => ((string)x).Split(','));
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Core/Users/UserMapper.cs b/src/Server/Coderr.Server.SqlServer/Core/Users/UserMapper.cs
new file mode 100644
index 00000000..510f1d9a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Core/Users/UserMapper.cs
@@ -0,0 +1,13 @@
+using Coderr.Server.Domain.Core.User;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Users
+{
+    public class UserMapper : CrudEntityMapper
+    {
+        public UserMapper() : base("Users")
+        {
+            Property(x => x.AccountId).PrimaryKey(false);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Core/Users/UserRepository.cs b/src/Server/Coderr.Server.SqlServer/Core/Users/UserRepository.cs
similarity index 86%
rename from src/Server/OneTrueError.SqlServer/Core/Users/UserRepository.cs
rename to src/Server/Coderr.Server.SqlServer/Core/Users/UserRepository.cs
index e7538712..845d0837 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Users/UserRepository.cs
+++ b/src/Server/Coderr.Server.SqlServer/Core/Users/UserRepository.cs
@@ -1,47 +1,48 @@
-using System.Data.Common;
-using System.Threading.Tasks;
-using Griffin.Container;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.App.Core.Users;
-
-namespace OneTrueError.SqlServer.Core.Users
-{
-    [Component]
-    public class UserRepository : IUserRepository
-    {
-        private readonly IAdoNetUnitOfWork _uow;
-
-        public UserRepository(IAdoNetUnitOfWork uow)
-        {
-            _uow = uow;
-        }
-
-        public async Task CreateAsync(User user)
-        {
-            using (var cmd = (DbCommand) _uow.CreateCommand())
-            {
-                cmd.CommandText = "INSERT INTO Users (AccountId, UserName, EmailAddress) VALUES(@id, @userName, @email)";
-                cmd.AddParameter("id", user.AccountId);
-                cmd.AddParameter("userName", user.UserName);
-                cmd.AddParameter("email", user.EmailAddress);
-                await cmd.ExecuteNonQueryAsync();
-            }
-        }
-
-        public async Task GetUserAsync(int accountId)
-        {
-            return await _uow.FirstAsync(new {AccountId = accountId});
-        }
-
-        public async Task UpdateAsync(User user)
-        {
-            await _uow.UpdateAsync(user);
-        }
-
-        public async Task FindByEmailAsync(string emailAddress)
-        {
-            return await _uow.FirstOrDefaultAsync("EmailAddress = @1", emailAddress);
-        }
-    }
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.User;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Core.Users
+{
+    [ContainerService]
+    public class UserRepository : IUserRepository
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public UserRepository(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task CreateAsync(User user)
+        {
+            using (var cmd = (DbCommand) _uow.CreateCommand())
+            {
+                cmd.CommandText = "INSERT INTO Users (AccountId, UserName, EmailAddress) VALUES(@id, @userName, @email)";
+                cmd.AddParameter("id", user.AccountId);
+                cmd.AddParameter("userName", user.UserName);
+                cmd.AddParameter("email", user.EmailAddress);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task GetUserAsync(int accountId)
+        {
+            return await _uow.FirstAsync(new {AccountId = accountId});
+        }
+
+        public async Task UpdateAsync(User user)
+        {
+            await _uow.UpdateAsync(user);
+        }
+
+        public async Task FindByEmailAsync(string emailAddress)
+        {
+            return await _uow.FirstOrDefaultAsync("EmailAddress = @1", emailAddress);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/DateTimeExtensions.cs b/src/Server/Coderr.Server.SqlServer/DateTimeExtensions.cs
similarity index 84%
rename from src/Server/OneTrueError.SqlServer/DateTimeExtensions.cs
rename to src/Server/Coderr.Server.SqlServer/DateTimeExtensions.cs
index 36bdcdfa..59f95304 100644
--- a/src/Server/OneTrueError.SqlServer/DateTimeExtensions.cs
+++ b/src/Server/Coderr.Server.SqlServer/DateTimeExtensions.cs
@@ -1,12 +1,12 @@
-using System;
-
-namespace OneTrueError.SqlServer
-{
-    public static class DateTimeExtensions
-    {
-        public static object ToDbNullable(this DateTime value)
-        {
-            return value == DateTime.MinValue ? (object) DBNull.Value : value;
-        }
-    }
+using System;
+
+namespace Coderr.Server.SqlServer
+{
+    public static class DateTimeExtensions
+    {
+        public static object ToDbNullable(this DateTime value)
+        {
+            return value == DateTime.MinValue ? (object) DBNull.Value : value;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/DbConverters.cs b/src/Server/Coderr.Server.SqlServer/DbConverters.cs
similarity index 90%
rename from src/Server/OneTrueError.SqlServer/DbConverters.cs
rename to src/Server/Coderr.Server.SqlServer/DbConverters.cs
index 9c079a5f..9dea4b55 100644
--- a/src/Server/OneTrueError.SqlServer/DbConverters.cs
+++ b/src/Server/Coderr.Server.SqlServer/DbConverters.cs
@@ -1,47 +1,47 @@
-using System;
-using OneTrueError.SqlServer.Tools;
-
-namespace OneTrueError.SqlServer
-{
-    public class DbConverters
-    {
-        public static bool BoolFromByteArray(object arg)
-        {
-            return Convert.ToBoolean(((byte[]) arg)[0]);
-        }
-
-        public static TProperty DeserializeEntity(object arg)
-        {
-            return EntitySerializer.Deserialize((string) arg);
-        }
-
-        public static object SerializeEntity(T arg)
-        {
-            return EntitySerializer.Serialize(arg);
-        }
-
-
-        public static DateTime ToEntityDate(object columnValue)
-        {
-            if (columnValue is DBNull)
-                return DateTime.MinValue;
-
-            return (DateTime) columnValue;
-        }
-
-        public static object ToNullableSqlDate(DateTime arg)
-        {
-            if (arg == DateTime.MinValue)
-                return DBNull.Value;
-
-            return arg;
-        }
-
-        public static object ToSqlNull(DateTime arg)
-        {
-            if (arg == DateTime.MinValue)
-                return DBNull.Value;
-            return arg;
-        }
-    }
+using System;
+using Coderr.Server.SqlServer.Tools;
+
+namespace Coderr.Server.SqlServer
+{
+    public class DbConverters
+    {
+        public static bool BoolFromByteArray(object arg)
+        {
+            return Convert.ToBoolean(((byte[]) arg)[0]);
+        }
+
+        public static TProperty DeserializeEntity(object arg)
+        {
+            return EntitySerializer.Deserialize((string) arg);
+        }
+
+        public static object SerializeEntity(T arg)
+        {
+            return EntitySerializer.Serialize(arg);
+        }
+
+
+        public static DateTime ToEntityDate(object columnValue)
+        {
+            if (columnValue is DBNull)
+                return DateTime.MinValue;
+
+            return (DateTime) columnValue;
+        }
+
+        public static object ToNullableSqlDate(DateTime arg)
+        {
+            if (arg == DateTime.MinValue)
+                return DBNull.Value;
+
+            return arg;
+        }
+
+        public static object ToSqlNull(DateTime arg)
+        {
+            if (arg == DateTime.MinValue)
+                return DBNull.Value;
+            return arg;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Logs/LogsRepository.cs b/src/Server/Coderr.Server.SqlServer/Logs/LogsRepository.cs
new file mode 100644
index 00000000..229dc019
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Logs/LogsRepository.cs
@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Modules.Logs;
+using Griffin.Data;
+using Newtonsoft.Json;
+
+namespace Coderr.Server.SqlServer.Logs
+{
+    [ContainerService]
+    public class LogsRepository : ILogsRepository
+    {
+        private IAdoNetUnitOfWork _unitOfWork;
+
+        public LogsRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task Exists(int incidentId, int? reportId)
+        {
+            if (reportId == null)
+            {
+                using (var cmd = _unitOfWork.CreateDbCommand())
+                {
+                    cmd.CommandText = "SELECT TOP(1) Id FROM ErrorReportLogs WHERE IncidentId = @id";
+                    cmd.AddParameter("id", incidentId);
+                    return await cmd.ExecuteScalarAsync() != DBNull.Value;
+                }
+            }
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT TOP(1) Id FROM ErrorReportLogs WHERE IncidentId = @incidentId AND ReportId = @reportId";
+                cmd.AddParameter("incidentId", incidentId);
+                cmd.AddParameter("reportId", reportId);
+                return await cmd.ExecuteScalarAsync() != null;
+            }
+        }
+
+        public async Task> Get(int incidentId, int? reportId)
+        {
+            string json;
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT TOP(1) Json FROM ErrorReportLogs WHERE IncidentId = @incidentId";
+                cmd.AddParameter("incidentId", incidentId);
+                if (reportId != null)
+                {
+                    cmd.CommandText += " AND ReportId = @reportId";
+                    cmd.AddParameter("reportId", reportId);
+                }
+
+                var obj = await cmd.ExecuteScalarAsync();
+                if (!(obj is string s))
+                {
+                    return new LogEntry[0];
+                }
+
+                json = s;
+            }
+
+            return JsonConvert.DeserializeObject(json);
+        }
+
+        public async Task Create(int incidentId, int reportId, IReadOnlyList entries)
+        {
+            var json = JsonConvert.SerializeObject(entries);
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "INSERT INTO ErrorReportLogs (Json, IncidentId, ReportId) VALUES(@json, @incidentId, @errorReportId)";
+                cmd.AddParameter("json", json);
+                cmd.AddParameter("errorReportId", reportId);
+                cmd.AddParameter("incidentId", incidentId);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Migrations/MigrationRunner.cs b/src/Server/Coderr.Server.SqlServer/Migrations/MigrationRunner.cs
new file mode 100644
index 00000000..9fed8bf3
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Migrations/MigrationRunner.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Data;
+using System.Data.SqlClient;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Migrations
+{
+    public class MigrationRunner
+    {
+        private readonly Func _connectionFactory;
+        private readonly string _scriptNamespace;
+        private readonly MigrationScripts _scripts;
+        private readonly ILog _logger = LogManager.GetLogger(typeof(MigrationRunner));
+        private readonly Assembly _scriptAssembly;
+
+        public MigrationRunner(Func connectionFactory, string migrationName, string scriptNamespace)
+        {
+            _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
+            MigrationName = migrationName ?? throw new ArgumentNullException(nameof(migrationName));
+            _scriptNamespace = scriptNamespace;
+            _scriptAssembly = GetScriptAssembly(migrationName);
+            _scripts = new MigrationScripts(migrationName, _scriptAssembly);
+        }
+
+        public string MigrationName { get; }
+
+        public int GetCurrentSchemaVersion()
+        {
+            string[] scripts =
+            {
+                @"IF OBJECT_ID (N'DatabaseSchema', N'U') IS NULL
+                        BEGIN
+                            CREATE TABLE [dbo].DatabaseSchema (
+                                [Version] int not null default 1,
+                                [Name] varchar(50) NOT NULL
+                            );
+                        END",
+                @"IF COL_LENGTH('DatabaseSchema', 'Name') IS NULL
+                    BEGIN
+                        ALTER TABLE DatabaseSchema ADD [Name] varchar(50) NULL;
+                    END;",
+                @"UPDATE DatabaseSchema SET Name = 'coderr' WHERE Name IS NULL"
+            };
+
+            using (var con = _connectionFactory())
+            {
+                foreach (var script in scripts)
+                {
+                    using (var cmd = con.CreateCommand())
+                    {
+                        cmd.CommandText = script;
+                        cmd.ExecuteNonQuery();
+                    }
+                }
+
+                using (var cmd = con.CreateCommand())
+                {
+                    try
+                    {
+                        cmd.CommandText = "SELECT Version FROM DatabaseSchema WHERE Name = @name";
+                        cmd.AddParameter("name", MigrationName);
+                        var result = cmd.ExecuteScalar();
+                        if (result is null)
+                            return -1;
+                        return (int)result;
+                    }
+                    catch (SqlException ex)
+                    {
+                        //invalid object name
+                        if (ex.Number == 208)
+                            return -1;
+
+                        throw;
+                    }
+                }
+            }
+        }
+
+        public int GetLatestSchemaVersion()
+        {
+            EnsureLoaded();
+            return _scripts.GetHighestVersion();
+        }
+
+        public void Run()
+        {
+            EnsureLoaded();
+            if (!CanSchemaBeUpgraded())
+            {
+                _logger.Debug("Db Schema is up to date.");
+                return;
+            }
+
+            _logger.Info("Updating DB schema.");
+            UpgradeDatabaseSchema();
+        }
+
+        /// 
+        ///     Check if the current DB schema is out of date compared to the embedded schema resources.
+        /// 
+        protected bool CanSchemaBeUpgraded()
+        {
+            var version = GetCurrentSchemaVersion();
+            var embeddedSchema = GetLatestSchemaVersion();
+            return embeddedSchema > version;
+        }
+
+        protected void LoadScripts()
+        {
+            var names =
+                _scriptAssembly
+                    .GetManifestResourceNames()
+                    .Where(x => x.StartsWith(_scriptNamespace) && x.Contains($"{MigrationName}.v"));
+
+            foreach (var name in names)
+            {
+                var pos = name.IndexOf(".v") + 2; //2 extra for ".v"
+                var endPos = name.IndexOf(".", pos);
+                var versionStr = name.Substring(pos, endPos - pos);
+                var version = int.Parse(versionStr);
+                _scripts.AddScriptName(version, name);
+            }
+        }
+
+        /// 
+        ///     Upgrade schema
+        /// 
+        /// -1 = latest version
+        protected void UpgradeDatabaseSchema(int toVersion = -1)
+        {
+            if (toVersion == -1)
+                toVersion = GetLatestSchemaVersion();
+            var currentSchema = GetCurrentSchemaVersion();
+            if (currentSchema < 1)
+                currentSchema = 0;
+
+            for (var version = currentSchema + 1; version <= toVersion; version++)
+            {
+                _logger.Info("Migrating to v" + version);
+                try
+                {
+                    using (var con = _connectionFactory())
+                    {
+                        _scripts.Execute(con, version);
+                        if (version == 1)
+                            con.ExecuteNonQuery(
+                                $"INSERT INTO DatabaseSchema (Name, Version) VALUES('{MigrationName}', 1)");
+                    }
+                }
+                catch (Exception ex)
+                {
+                    throw new InvalidDataException($"Failed to run script {MigrationName} v" + version, ex);
+                }
+            }
+        }
+
+        private void EnsureLoaded()
+        {
+            if (_scripts.IsEmpty)
+                LoadScripts();
+        }
+
+        private Assembly GetScriptAssembly(string migrationName)
+        {
+            if (migrationName == null) throw new ArgumentNullException(nameof(migrationName));
+
+            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
+            {
+                if (assembly.IsDynamic)
+                    continue;
+                if (assembly.GetName().Name?.StartsWith("Coderr", StringComparison.OrdinalIgnoreCase) != true)
+                    continue;
+
+                var isFound = assembly
+                    .GetManifestResourceNames()
+                    .Any(x => x.StartsWith(_scriptNamespace) && x.Contains($"{MigrationName}.v"));
+                if (isFound)
+                    return assembly;
+            }
+
+            throw new InvalidOperationException($"Failed to find scripts for migration '{migrationName}'.");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Migrations/MigrationScripts.cs b/src/Server/Coderr.Server.SqlServer/Migrations/MigrationScripts.cs
new file mode 100644
index 00000000..039630ac
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Migrations/MigrationScripts.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+
+namespace Coderr.Server.SqlServer.Migrations
+{
+    public class MigrationScripts
+    {
+        private readonly string _migrationName;
+        private readonly Dictionary _versions = new Dictionary();
+        private Assembly _scriptAssembly;
+        public MigrationScripts(string migrationName, Assembly scriptAssembly)
+        {
+            _migrationName = migrationName ?? throw new ArgumentNullException(nameof(migrationName));
+            _scriptAssembly = scriptAssembly ?? throw new ArgumentNullException(nameof(scriptAssembly));
+        }
+
+        public MigrationScripts(string migrationName)
+        {
+            _migrationName = migrationName ?? throw new ArgumentNullException(nameof(migrationName));
+        }
+
+        public bool IsEmpty => _versions.Count == 0;
+
+        public void AddScriptName(int version, string scriptName)
+        {
+            if (version < 0) throw new ArgumentOutOfRangeException(nameof(version));
+
+            _versions[version] = scriptName ?? throw new ArgumentNullException(nameof(scriptName));
+        }
+
+        public string LoadScript(int version)
+        {
+            if (version <= 0) throw new ArgumentOutOfRangeException(nameof(version));
+
+            var scriptName = _versions[version];
+            var res = _scriptAssembly.GetManifestResourceStream(scriptName);
+            if (res == null)
+                throw new InvalidOperationException("Failed to find schema " + scriptName);
+
+            return new StreamReader(res).ReadToEnd();
+        }
+
+        public void Execute(IDbConnection connection, int version)
+        {
+            var script = LoadScript(version);
+            var sb = new StringBuilder();
+            var sr = new StringReader(script);
+            while (true)
+            {
+                var line = sr.ReadLine();
+                if (line == null)
+                    break;
+
+                if (!line.Equals("go"))
+                {
+                    sb.AppendLine(line);
+                    continue;
+                }
+
+                ExecuteSql(connection, sb.ToString());
+                sb.Clear();
+
+            }
+
+            //do the remaining part of the script (or everything if GO was not used).
+            ExecuteSql(connection, sb.ToString());
+
+            ExecuteSql(connection, $"UPDATE DatabaseSchema SET Version={version} WHERE Name = '{_migrationName}'");
+        }
+
+        private static void ExecuteSql(IDbConnection connection, string sql)
+        {
+            var parts = sql.Split(new[] {"\r\ngo\r\n", "\r\nGO\r\n", "\r\ngo;\r\n"},
+                StringSplitOptions.RemoveEmptyEntries);
+            foreach (var part in parts)
+            {
+                using (var transaction = connection.BeginTransaction())
+                {
+                    using (var cmd = connection.CreateCommand())
+                    {
+                        cmd.Transaction = transaction;
+                        cmd.CommandText = part;
+                        cmd.ExecuteNonQuery();
+                    }
+
+                    transaction.Commit();
+                }
+            }
+        }
+
+        public int GetHighestVersion()
+        {
+            if (_versions.Count == 0)
+                return -1;
+            return _versions.Max(x => x.Key);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginListItem.cs b/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginListItem.cs
new file mode 100644
index 00000000..2cdebed8
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginListItem.cs
@@ -0,0 +1,23 @@
+namespace Coderr.Server.SqlServer.Modules.Geolocation
+{
+    /// 
+    ///     List result item for repository queries
+    /// 
+    public class ErrorOriginListItem
+    {
+        /// 
+        ///     Latitude
+        /// 
+        public double Latitude { get; set; }
+
+        /// 
+        ///     Longitude
+        /// 
+        public double Longitude { get; set; }
+
+        /// 
+        ///     Number of error reports that have been received from this incident
+        /// 
+        public int NumberOfErrorReports { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginMapper.cs
new file mode 100644
index 00000000..ce635a34
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginMapper.cs
@@ -0,0 +1,20 @@
+using System;
+using Coderr.Server.Domain.Modules.ErrorOrigins;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Geolocation
+{
+    public class ErrorOriginMapper : CrudEntityMapper
+    {
+        public ErrorOriginMapper() : base("ErrorOrigins")
+        {
+            Property(x => x.Longitude)
+                .ToColumnValue2(x => Convert.ToDecimal(x.Value))
+                .ToPropertyValue2(x => Convert.ToDouble(x.Value));
+            Property(x => x.Latitude)
+                .ToColumnValue2(x => Convert.ToDecimal(x.Value))
+                .ToPropertyValue2(x => Convert.ToDouble(x.Value));
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginRepository.cs b/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginRepository.cs
new file mode 100644
index 00000000..ef012261
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/ErrorOriginRepository.cs
@@ -0,0 +1,160 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Modules.ErrorOrigins;
+using Coderr.Server.ReportAnalyzer.ErrorOrigins;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Geolocation
+{
+    [ContainerService]
+    public class ErrorOriginRepository : IErrorOriginRepository
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public ErrorOriginRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task CreateAsync(ErrorOrigin entity, int applicationId, int incidentId, int reportId)
+        {
+            if (entity == null) throw new ArgumentNullException(nameof(entity));
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId));
+            if (incidentId <= 0) throw new ArgumentOutOfRangeException(nameof(incidentId));
+            if (reportId <= 0) throw new ArgumentOutOfRangeException(nameof(reportId));
+
+            if (entity.IpAddress != null)
+            {
+                using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+                {
+                    cmd.CommandText = "SELECT Id FROM ErrorOrigins WHERE IpAddress = @ip";
+                    cmd.AddParameter("ip", entity.IpAddress);
+                    var id = await cmd.ExecuteScalarAsync();
+                    if (id is int)
+                    {
+                        await CreateReportInfoAsync((int)id, applicationId, incidentId, reportId);
+                        return;
+                    }
+                }
+            }
+            if (entity.Latitude < ErrorOrigin.EmptyLatitude && entity.Longitude < ErrorOrigin.EmptyLongitude)
+            {
+                using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+                {
+                    cmd.CommandText = "SELECT Id FROM ErrorOrigins WHERE Longitude = @long AND Latitude = @lat";
+                    cmd.AddParameter("long", entity.Longitude);
+                    cmd.AddParameter("lat", entity.Latitude);
+                    var id = await cmd.ExecuteScalarAsync();
+                    if (id is int)
+                    {
+                        await CreateReportInfoAsync((int)id, applicationId, incidentId, reportId);
+                        return;
+                    }
+                }
+            }
+
+            using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = "INSERT INTO ErrorOrigins (IpAddress, CountryCode, CountryName, RegionCode, RegionName, City, ZipCode, Latitude, Longitude, CreatedAtUtc, IsLookedUp) " +
+                                  "VALUES (@IpAddress, @CountryCode, @CountryName, @RegionCode, @RegionName, @City, @ZipCode, @Latitude, @Longitude, @CreatedAtUtc, 0);select cast(SCOPE_IDENTITY() as int);";
+                cmd.AddParameter("IpAddress", entity.IpAddress);
+                cmd.AddParameter("CountryCode", entity.CountryCode);
+                cmd.AddParameter("CountryName", entity.CountryName);
+                cmd.AddParameter("RegionCode", entity.RegionCode);
+                cmd.AddParameter("RegionName", entity.RegionName);
+                cmd.AddParameter("City", entity.City);
+                cmd.AddParameter("ZipCode", entity.ZipCode);
+                //cmd.AddParameter("Point", SqlGeography.Point(command.Latitude, command.Longitude, 4326));
+                cmd.AddParameter("Latitude", entity.Latitude);
+                cmd.AddParameter("Longitude", entity.Longitude);
+                cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow);
+                var id = (int)await cmd.ExecuteScalarAsync();
+                entity.Id = id;
+                await CreateReportInfoAsync(id, applicationId, incidentId, reportId);
+            }
+        }
+
+        public Task> GetPendingOrigins()
+        {
+            using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"SELECT TOP(50) * FROM ErrorOrigins WHERE IsLookedUp = 0";
+                return cmd.ToListAsync();
+            }
+        }
+
+        public async Task Update(ErrorOrigin entity)
+        {
+            using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = "UPDATE ErrorOrigins SET " +
+                                  "CountryCode=@CountryCode, " +
+                                  "CountryName=@CountryName, " +
+                                  "RegionCode=@RegionCode, " +
+                                  "RegionName=@RegionName, " +
+                                  "City=@City, " +
+                                  "ZipCode=@ZipCode, " +
+                                  "Latitude=@Latitude, " +
+                                  "Longitude=@Longitude, " +
+                                  "IsLookedUp = 1 " +
+                                  "WHERE Id = @id";
+                cmd.AddParameter("CountryCode", entity.CountryCode);
+                cmd.AddParameter("CountryName", entity.CountryName);
+                cmd.AddParameter("RegionCode", entity.RegionCode);
+                cmd.AddParameter("RegionName", entity.RegionName);
+                cmd.AddParameter("City", entity.City);
+                cmd.AddParameter("ZipCode", entity.ZipCode);
+                cmd.AddParameter("Latitude", entity.Latitude);
+                cmd.AddParameter("Longitude", entity.Longitude);
+                cmd.AddParameter("Id", entity.Id);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task> FindForIncidentAsync(int incidentId)
+        {
+            using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"SELECT Longitude, Latitude, count(*) 
+                                    FROM ErrorOrigins eo
+                                    JOIN ErrorReportOrigins ON (eo.Id = ErrorReportOrigins.ErrorOriginId)
+                                    WHERE IncidentId = @id AND IsLookedUp = 1
+                                    GROUP BY IncidentId, Longitude, Latitude";
+                cmd.AddParameter("id", incidentId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var items = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new ErrorOriginListItem
+                        {
+                            Longitude = (double)reader.GetDecimal(0),
+                            Latitude = (double)reader.GetDecimal(1),
+                            NumberOfErrorReports = reader.GetInt32(2)
+                        };
+                        items.Add(item);
+                    }
+                    return items;
+                }
+            }
+        }
+
+        private async Task CreateReportInfoAsync(int originId, int applicationId, int incidentId, int reportId)
+        {
+            using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "INSERT INTO ErrorReportOrigins (ErrorOriginId, ApplicationId, IncidentId, ReportId, CreatedAtUtc) VALUES(@oid, @aid, @iid, @rid, GetUtcDate())";
+                cmd.AddParameter("oid", originId);
+                cmd.AddParameter("aid", applicationId);
+                cmd.AddParameter("iid", incidentId);
+                cmd.AddParameter("rid", reportId);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/GetOriginsForIncidentHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/GetOriginsForIncidentHandler.cs
new file mode 100644
index 00000000..38498c38
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Geolocation/GetOriginsForIncidentHandler.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.ErrorOrigins.Queries;
+using DotNetCqs;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Modules.Geolocation
+{
+    /// 
+    ///     Handler for .
+    /// 
+    public class GetOriginsForIncidentHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+
+        /// 
+        ///     Creates a new instance of .
+        /// 
+        /// repository
+        public GetOriginsForIncidentHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        /// 
+        ///     Method used to execute the query
+        /// 
+        /// Query to execute.
+        /// 
+        ///     Task which will contain the result once completed.
+        /// 
+        public async Task HandleAsync(IMessageContext context, GetOriginsForIncident query)
+        {
+            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"SELECT Longitude, Latitude, count(*) 
+                                    FROM ErrorOrigins eo
+                                    JOIN ErrorReportOrigins ON (eo.Id = ErrorReportOrigins.ErrorOriginId)
+                                    WHERE IncidentId = @id AND eo.IsLookedUp = 1
+                                    GROUP BY IncidentId, Longitude, Latitude";
+                cmd.AddParameter("id", query.IncidentId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var items = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new GetOriginsForIncidentResultItem
+                        {
+                            Longitude = (double) reader.GetDecimal(0),
+                            Latitude = (double) reader.GetDecimal(1),
+                            NumberOfErrorReports = reader.GetInt32(2)
+                        };
+                        items.Add(item);
+                    }
+
+                    return new GetOriginsForIncidentResult {Items = items.ToArray()};
+                    ;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/History/DeleteAbandonedHistory.cs b/src/Server/Coderr.Server.SqlServer/Modules/History/DeleteAbandonedHistory.cs
new file mode 100644
index 00000000..60f53a9e
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/History/DeleteAbandonedHistory.cs
@@ -0,0 +1,30 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Modules.History
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class DeleteAbandonedHistory : IBackgroundJobAsync
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public DeleteAbandonedHistory(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"DELETE FROM IncidentHistory
+                                    FROM IncidentHistory
+                                    LEFT JOIN Incidents ON (Incidents.Id = IncidentId)
+                                    WHERE Incidents.Id IS NULL";
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/History/GetIncidentsForStatesHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/History/GetIncidentsForStatesHandler.cs
new file mode 100644
index 00000000..e6306d80
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/History/GetIncidentsForStatesHandler.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.History.Queries;
+using Coderr.Server.Domain.Core.Incidents;
+using DotNetCqs;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Modules.History
+{
+    internal class GetIncidentsForStatesHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetIncidentsForStatesHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetIncidentsForStates query)
+        {
+            var sql =
+                @"select i.Id, i.Description, i.FullName, i.CreatedAtUtc, i.AssignedAtUtc, i.SolvedAtUtc, IncidentHistory.State HistoryState
+                        from incidents i
+                        join IncidentHistory on (i.Id= IncidentHistory.IncidentId)
+                        where ApplicationVersion = @version
+                        AND i.ApplicationId = @appId
+                        AND IncidentHistory.State in (0, 3, 4)";
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = sql;
+                cmd.AddParameter("appId", query.ApplicationId);
+                cmd.AddParameter("version", query.ApplicationVersion);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var items = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var state = (IncidentState) reader["HistoryState"];
+                        var item = new GetIncidentsForStatesResultItem
+                        {
+                            CreatedAtUtc = (DateTime) reader["CreatedAtUtc"],
+                            IncidentId = (int) reader["Id"],
+                            IncidentName = GetIncidentName((string) reader["Description"]),
+                            IsClosed = state == IncidentState.Closed,
+                            IsNew = state == IncidentState.New,
+                            IsReopened = state == IncidentState.ReOpened
+                        };
+                        items.Add(item);
+                    }
+
+                    return new GetIncidentsForStatesResult {Items = items.ToArray()};
+                }
+            }
+        }
+
+        private string GetIncidentName(string description)
+        {
+            var pos = description.IndexOfAny(new[] {'\r', '\n'});
+            if (pos != -1)
+                return description.Substring(0, pos);
+            return description;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/History/HistoryEntryMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/History/HistoryEntryMapper.cs
new file mode 100644
index 00000000..d99432a5
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/History/HistoryEntryMapper.cs
@@ -0,0 +1,21 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Coderr.Server.Domain.Modules.History;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.History
+{
+    class HistoryEntryMapper : CrudEntityMapper
+    {
+        public HistoryEntryMapper() : base("IncidentHistory")
+        {
+            Property(x => x.Id).PrimaryKey(true);
+            Property(x => x.IncidentState)
+                .ColumnName("State");
+            Property(x => x.AccountId)
+                .ToColumnValue(x => (object)x ?? DBNull.Value)
+                .ToPropertyValue(x => (int?)(x is DBNull ? null : x));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/History/HistoryRepository.cs b/src/Server/Coderr.Server.SqlServer/Modules/History/HistoryRepository.cs
new file mode 100644
index 00000000..feb758e0
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/History/HistoryRepository.cs
@@ -0,0 +1,31 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Modules.History;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.History
+{
+    [ContainerService]
+    internal class HistoryRepository : IHistoryRepository
+    {
+        private IAdoNetUnitOfWork _unitOfWork;
+
+        public HistoryRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task CreateAsync(HistoryEntry entry)
+        {
+            await _unitOfWork.InsertAsync(entry);
+        }
+
+        public async Task> GetByIncidentId(int incidentId)
+        {
+            return await _unitOfWork.ToListAsync("IncidentId = @0", incidentId);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Mine/Incidents/IncidentWithMostReportsProvider.cs b/src/Server/Coderr.Server.SqlServer/Modules/Mine/Incidents/IncidentWithMostReportsProvider.cs
new file mode 100644
index 00000000..65b3389c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Mine/Incidents/IncidentWithMostReportsProvider.cs
@@ -0,0 +1,50 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.App.Modules.Mine;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Mine.Incidents
+{
+    [ContainerService]
+    class IncidentWithMostReportsProvider : IRecommendationProvider
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public IncidentWithMostReportsProvider(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task Recommend(RecommendIncidentContext context)
+        {
+            var sql = context.ApplicationId > 0
+                ? $@"SELECT TOP(5) Incidents.Id IncidentId, Applications.Id as ApplicationId, Applications.Name as ApplicationName, ReportCount as Score, 'Replace' as Motivation
+                                FROM Incidents 
+                                JOIN Applications ON (Applications.Id = Incidents.ApplicationId)
+                                WHERE State = 0
+                                AND ApplicationId = {context.ApplicationId.Value}
+                                ORDER BY ReportCount DESC"
+                : $@"SELECT TOP(5) Incidents.Id IncidentId, Applications.Id as ApplicationId, Applications.Name as ApplicationName, ReportCount as Score, 'Replace' as Motivation
+                                FROM Incidents 
+                                JOIN Applications ON (Applications.Id = Incidents.ApplicationId)
+                                WHERE State = 0
+                                ORDER BY ReportCount DESC";
+
+            var items = await _uow.ToListAsync(new MirrorMapper(), sql);
+            if (!items.Any())
+                return;
+
+            var totalReports = (double)items.Sum(x => x.Score);
+            foreach (var item in items)
+            {
+                var count = item.Score;
+                item.Score = (int)((item.Score / totalReports) * 100);
+                item.Motivation = $"Frequently reported ({count:N0} times)";
+                context.Add(item, 1);
+            }
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMyIncidentsQueryHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMyIncidentsQueryHandler.cs
new file mode 100644
index 00000000..8fa05bc6
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMyIncidentsQueryHandler.cs
@@ -0,0 +1,154 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Api.Modules.Mine.Queries;
+using Coderr.Server.App.Modules.Mine;
+using Coderr.Server.Domain.Core.Applications;
+using Coderr.Server.Domain.Core.Incidents;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Mine
+{
+    public class ListMyIncidentsQueryHandler : IQueryHandler
+    {
+        private readonly IIncidentRepository _repository;
+        private readonly IRecommendationService _recommendationService;
+        private readonly IAdoNetUnitOfWork _uow;
+        private IApplicationRepository _applicationRepository;
+
+        public ListMyIncidentsQueryHandler(IAdoNetUnitOfWork uow, IRecommendationService recommendationService,
+            IIncidentRepository repository, IApplicationRepository applicationRepository)
+        {
+            _uow = uow;
+            _recommendationService = recommendationService ??
+                                       throw new ArgumentNullException(nameof(recommendationService));
+            _repository = repository;
+            _applicationRepository = applicationRepository;
+        }
+
+        public async Task HandleAsync(IMessageContext context, ListMyIncidents query)
+        {
+            var totalCount = await GetTotalCount(context);
+            var incidents = await GetMyItems(context);
+            var suggestions = new List();
+            var items = await _recommendationService.GetRecommendations(context.Principal.GetAccountId(), query.ApplicationId);
+            if (items.Any())
+            {
+                var enriched = await EnrichSuggestions(items);
+                suggestions.AddRange(enriched);
+            }
+
+            string comment = "";
+            if (incidents.Count == 0)
+            {
+                if (suggestions.Count > 0)
+                {
+                    comment = "It's time to do some work. Why don't you start with the selected incident below?";
+                }
+            }
+            else
+            {
+                var oldestIncident = incidents.OrderByDescending(x => x.DaysOld).First();
+                if (oldestIncident.DaysOld > 7)
+                {
+                    comment =
+                        $"Your oldest incident is {oldestIncident.DaysOld} days old. Why don't you solve it before doing something else? Solve it";
+                }
+                else if (incidents.Count <= 3)
+                {
+                    comment = $"";
+                }
+                else if (incidents.Count > 3)
+                {
+                    var random = new Random();
+                    comment = random.Next(0, 2) == 0
+                        ? "Did you know that Coderr ignores reports for closed incidents as long as they are for older application versions? Solve some.."
+                        : "We recommend that you correct existing incidents before taking new. Click 'Analyze' in the top menu.";
+                }
+            }
+
+            var result = new ListMyIncidentsResult
+            {
+                Comment = comment,
+                Items = incidents,
+                Suggestions = suggestions
+            };
+
+            return result;
+        }
+
+        private async Task> EnrichSuggestions(List items)
+        {
+            var incidentIds = items.Select(x => x.IncidentId).Distinct();
+            var incidents = await _repository.GetManyAsync(incidentIds);
+            var apps = new Dictionary();
+            var result = new List();
+            foreach (var item in items)
+            {
+                var incident = incidents.First(x => x.Id == item.IncidentId);
+                var suggestion = new ListMySuggestedItem(incident.Id, incident.Description)
+                {
+                    ApplicationId = incident.ApplicationId,
+                    ApplicationName = item.ApplicationName,
+                    CreatedAtUtc = incident.CreatedAtUtc,
+                    ExceptionTypeName = incident.FullName,
+                    LastReportAtUtc = incident.LastReportAtUtc,
+                    Motivation = item.Motivation,
+                    StackTrace = incident.StackTrace,
+                    ReportCount = incident.ReportCount
+                };
+
+                if (suggestion.ApplicationId == 0 || string.IsNullOrEmpty(suggestion.ApplicationName))
+                {
+                    if (!apps.TryGetValue(suggestion.ApplicationId, out var app))
+                    {
+                        app = await _applicationRepository.GetByIdAsync(suggestion.ApplicationId);
+                        apps[app.Id] = app;
+                    }
+
+                    suggestion.ApplicationName = app.Name;
+                }
+                result.Add(suggestion);
+            }
+
+            return result;
+        }
+
+
+        private async Task> GetMyItems(IMessageContext context)
+        {
+            var sqlQuery =
+                $@"SELECT Incidents.*, Incidents.Description as Name, Applications.Id as ApplicationId, Applications.Name as ApplicationName
+                                FROM Incidents 
+                                JOIN Applications ON (Applications.Id = Incidents.ApplicationId)
+                                WHERE State = {(int)IncidentState.Active}
+                                AND AssignedToId=@userId";
+
+            var incidents =
+                await _uow.ToListAsync(new ListMyIncidentsResultItemMapper(), sqlQuery,
+                    new { userId = context.Principal.GetAccountId() });
+            return incidents;
+        }
+
+        private async Task GetTotalCount(IMessageContext context)
+        {
+            var sqlQuery = $@"SELECT cast(count(*) as int)
+FROM Incidents i
+JOIN Applications a ON (a.Id = i.ApplicationId)
+JOIN ApplicationMembers am ON (a.Id = am.ApplicationId)
+WHERE am.ApplicationId = @accountId
+";
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.AddParameter("accountId", context.Principal.GetAccountId());
+                cmd.CommandText = sqlQuery;
+                return (int)await cmd.ExecuteScalarAsync();
+            }
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMyIncidentsResultItemMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMyIncidentsResultItemMapper.cs
new file mode 100644
index 00000000..b0c7a141
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMyIncidentsResultItemMapper.cs
@@ -0,0 +1,13 @@
+using Coderr.Server.Api.Modules.Mine.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Mine
+{
+    internal class ListMyIncidentsResultItemMapper : EntityMapper
+    {
+        public ListMyIncidentsResultItemMapper()
+        {
+            Property(x => x.DaysOld).Ignore();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMySuggestedItemMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMySuggestedItemMapper.cs
new file mode 100644
index 00000000..f9467adf
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Mine/ListMySuggestedItemMapper.cs
@@ -0,0 +1,13 @@
+using Coderr.Server.Api.Modules.Mine.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Mine
+{
+    internal class ListMySuggestedItemMapper : EntityMapper
+    {
+        public ListMySuggestedItemMapper()
+        {
+            Property(x => x.ExceptionTypeName).ColumnName("FullName");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/ReportSpikes/ErrorReportSpikeMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/ReportSpikes/ErrorReportSpikeMapper.cs
new file mode 100644
index 00000000..0247d682
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/ReportSpikes/ErrorReportSpikeMapper.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Coderr.Server.Domain.Modules.ReportSpikes;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.ReportSpikes
+{
+    public class ErrorReportSpikeMapper: CrudEntityMapper
+
+    {
+        public ErrorReportSpikeMapper() : base("ErrorReportSpikes")
+        {
+            Property(x => x.NotifiedAccounts)
+                .ToColumnValue(x => string.Join(",", x))
+                .ToPropertyValue(x => ((string) x)
+                    .Split(',')
+                    .Select(int.Parse)
+                    .ToArray()
+                );
+        }
+    }
+}
diff --git a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs
similarity index 79%
rename from src/Server/OneTrueError.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs
rename to src/Server/Coderr.Server.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs
index 65ce989b..77672798 100644
--- a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyDbEntity.cs
@@ -1,21 +1,21 @@
-using System.Linq;
-using OneTrueError.App.Modules.Similarities.Domain;
-
-namespace OneTrueError.SqlServer.Modules.Similarities.Entities
-{
-    public class ContextCollectionPropertyDbEntity
-    {
-        public ContextCollectionPropertyDbEntity()
-        {
-        }
-
-        public ContextCollectionPropertyDbEntity(Similarity similarity)
-        {
-            Name = similarity.PropertyName;
-            Values = similarity.Values.Select(x => new ContextCollectionPropertyValueDbEntity(x)).ToArray();
-        }
-
-        public string Name { get; set; }
-        public ContextCollectionPropertyValueDbEntity[] Values { get; set; }
-    }
+using System.Linq;
+using Coderr.Server.Domain.Modules.Similarities;
+
+namespace Coderr.Server.SqlServer.Modules.Similarities.Entities
+{
+    public class ContextCollectionPropertyDbEntity
+    {
+        public ContextCollectionPropertyDbEntity()
+        {
+        }
+
+        public ContextCollectionPropertyDbEntity(Similarity similarity)
+        {
+            Name = similarity.PropertyName;
+            Values = similarity.Values.Select(x => new ContextCollectionPropertyValueDbEntity(x)).ToArray();
+        }
+
+        public string Name { get; set; }
+        public ContextCollectionPropertyValueDbEntity[] Values { get; set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs
similarity index 79%
rename from src/Server/OneTrueError.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs
rename to src/Server/Coderr.Server.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs
index 42688498..ee3d1467 100644
--- a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Entities/ContextCollectionPropertyValueDbEntity.cs
@@ -1,26 +1,26 @@
-using System;
-using OneTrueError.App.Modules.Similarities.Domain;
-
-namespace OneTrueError.SqlServer.Modules.Similarities.Entities
-{
-    public class ContextCollectionPropertyValueDbEntity
-    {
-        public ContextCollectionPropertyValueDbEntity(SimilarityValue x)
-        {
-            if (x == null) throw new ArgumentNullException(nameof(x));
-            Value = x.Value;
-            Count = x.Count;
-            Percentage = x.Percentage;
-        }
-
-        protected ContextCollectionPropertyValueDbEntity()
-        {
-        }
-
-
-        public int Count { get; set; }
-
-        public int Percentage { get; set; }
-        public string Value { get; set; }
-    }
+using System;
+using Coderr.Server.Domain.Modules.Similarities;
+
+namespace Coderr.Server.SqlServer.Modules.Similarities.Entities
+{
+    public class ContextCollectionPropertyValueDbEntity
+    {
+        public ContextCollectionPropertyValueDbEntity(SimilarityValue x)
+        {
+            if (x == null) throw new ArgumentNullException(nameof(x));
+            Value = x.Value;
+            Count = x.Count;
+            Percentage = x.Percentage;
+        }
+
+        protected ContextCollectionPropertyValueDbEntity()
+        {
+        }
+
+
+        public int Count { get; set; }
+
+        public int Percentage { get; set; }
+        public string Value { get; set; }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs
new file mode 100644
index 00000000..d114444c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs
@@ -0,0 +1,22 @@
+using Coderr.Server.Domain.Modules.Similarities;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Similarities.Mappers
+{
+    public class SimilarityCollectionMapper : CrudEntityMapper
+    {
+        public SimilarityCollectionMapper()
+            : base("SimilarityCollections")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+
+            Property(x => x.Properties)
+                .NotForCrud()
+                .NotForQueries();
+
+            Property(x => x.Name)
+                .ColumnName("ContextName");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityMapper.cs
new file mode 100644
index 00000000..bef21e99
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityMapper.cs
@@ -0,0 +1,22 @@
+using Coderr.Server.Domain.Modules.Similarities;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Similarities.Mappers
+{
+    public class SimilarityMapper : CrudEntityMapper
+    {
+        public SimilarityMapper()
+            : base("Similarities")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+
+            Property(x => x.PropertyName)
+                .ColumnName("Name");
+
+            Property(x => x.Values)
+                .NotForCrud()
+                .NotForQueries();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityValueMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityValueMapper.cs
new file mode 100644
index 00000000..6d7bd5fc
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Mappers/SimilarityValueMapper.cs
@@ -0,0 +1,14 @@
+using Coderr.Server.Domain.Modules.Similarities;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Similarities.Mappers
+{
+    public class SimilarityValueMapper : CrudEntityMapper
+    {
+        public SimilarityValueMapper()
+            : base("SimilarityValues")
+        {
+            //Property(x => x.Id).PrimaryKey(true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Queries/GetSimilaritiesHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Queries/GetSimilaritiesHandler.cs
similarity index 76%
rename from src/Server/OneTrueError.SqlServer/Modules/Similarities/Queries/GetSimilaritiesHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Modules/Similarities/Queries/GetSimilaritiesHandler.cs
index 827e0d02..f36824f4 100644
--- a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Queries/GetSimilaritiesHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/Queries/GetSimilaritiesHandler.cs
@@ -1,55 +1,54 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using log4net;
-using OneTrueError.Api.Modules.ContextData.Queries;
-using OneTrueError.Infrastructure;
-using OneTrueError.SqlServer.Modules.Similarities.Entities;
-
-namespace OneTrueError.SqlServer.Modules.Similarities.Queries
-{
-    [Component]
-    public class GetSimilaritiesHandler : IQueryHandler
-    {
-        private readonly ILog _logger = LogManager.GetLogger(typeof(GetSimilaritiesHandler));
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public GetSimilaritiesHandler(IAdoNetUnitOfWork unitOfWork)
-        {
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task ExecuteAsync(GetSimilarities query)
-        {
-            using (var cmd = _unitOfWork.CreateDbCommand())
-            {
-                cmd.CommandText =
-                    @"select Name, Properties from IncidentContextCollections 
-                            where IncidentId = @incidentId";
-                cmd.AddParameter("incidentId", query.IncidentId);
-
-                var collections = new List();
-                using (var reader = await cmd.ExecuteReaderAsync())
-                {
-                    while (await reader.ReadAsync())
-                    {
-                        var json = (string) reader["Properties"];
-                        var properties = OneTrueSerializer.Deserialize(json);
-                        var col = new GetSimilaritiesCollection {Name = reader.GetString(0)};
-                        col.Similarities = (from prop in properties
-                            let values =
-                                prop.Values.Select(x => new GetSimilaritiesValue(x.Value, x.Percentage, x.Count))
-                            select new GetSimilaritiesSimilarity(prop.Name) {Values = values.ToArray()}
-                            ).ToArray();
-                        collections.Add(col);
-                    }
-                }
-
-                return new GetSimilaritiesResult {Collections = collections.ToArray()};
-            }
-        }
-    }
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.ContextData.Queries;
+using Coderr.Server.Infrastructure;
+using Coderr.Server.SqlServer.Modules.Similarities.Entities;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Modules.Similarities.Queries
+{
+    public class GetSimilaritiesHandler : IQueryHandler
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(GetSimilaritiesHandler));
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetSimilaritiesHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetSimilarities query)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    @"select Name, Properties from IncidentContextCollections 
+                            where IncidentId = @incidentId";
+                cmd.AddParameter("incidentId", query.IncidentId);
+
+                var collections = new List();
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var json = (string) reader["Properties"];
+                        var properties = CoderrDtoSerializer.Deserialize(json);
+                        var col = new GetSimilaritiesCollection {Name = reader.GetString(0)};
+                        col.Similarities = (from prop in properties
+                            let values =
+                                prop.Values.Select(x => new GetSimilaritiesValue(x.Value, x.Percentage, x.Count))
+                            select new GetSimilaritiesSimilarity(prop.Name) {Values = Enumerable.ToArray(values)}
+                            ).ToArray();
+                        collections.Add(col);
+                    }
+                }
+
+                return new GetSimilaritiesResult {Collections = collections.ToArray()};
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Similarities/SimilarityRepository.cs b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/SimilarityRepository.cs
new file mode 100644
index 00000000..3e129945
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Similarities/SimilarityRepository.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Modules.Similarities;
+using Coderr.Server.Infrastructure;
+using Coderr.Server.ReportAnalyzer.Similarities.Handlers.Processing;
+using Coderr.Server.SqlServer.Modules.Similarities.Entities;
+using Coderr.Server.SqlServer.Tools;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Modules.Similarities
+{
+    [ContainerService]
+    public class SimilarityRepository : ISimilarityRepository
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+        private ILog _logger = LogManager.GetLogger(typeof(SimilarityRepository));
+
+        public SimilarityRepository(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow ?? throw new ArgumentNullException(nameof(uow));
+        }
+
+        public async Task CreateAsync(SimilaritiesReport similarity)
+        {
+            foreach (var collection in similarity.Collections)
+            {
+                var dto = collection.Properties.Select(x => new ContextCollectionPropertyDbEntity(x)).ToArray();
+                var json = EntitySerializer.Serialize(dto);
+                if (collection.Id == 0)
+                {
+                    using (var cmd = _uow.CreateDbCommand())
+                    {
+                        cmd.CommandText =
+                            @"INSERT INTO IncidentContextCollections (IncidentId, Name, Properties) 
+                      VALUES(@incidentId, @name, @props)";
+                        cmd.AddParameter("incidentId", collection.IncidentId);
+                        cmd.AddParameter("name", collection.Name);
+                        cmd.AddParameter("props", json);
+                        await cmd.ExecuteNonQueryAsync();
+                    }
+                }
+                else
+                {
+                    using (var cmd = _uow.CreateDbCommand())
+                    {
+                        cmd.CommandText =
+                            @"UPDATE IncidentContextCollections SET Properties=@props WHERE Id = @id";
+                        cmd.AddParameter("id", collection.Id);
+                        cmd.AddParameter("props", json);
+                        await cmd.ExecuteNonQueryAsync();
+                    }
+                }
+            }
+        }
+
+        public SimilaritiesReport FindForIncident(int incidentId)
+        {
+            using (var cmd = _uow.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"select Id, Name, Properties from IncidentContextCollections 
+                            where IncidentId = @incidentId";
+                cmd.AddParameter("incidentId", incidentId);
+
+                var collections = new List();
+
+                using (var reader = cmd.ExecuteReader())
+                {
+                    while (reader.Read())
+                    {
+                        var json = (string) reader["Properties"];
+                        var properties = CoderrDtoSerializer.Deserialize(json);
+                        foreach (var property in properties)
+                        {
+                            var zeroProps = property.Values.Where(x => x.Count == 0);
+                            foreach (var prop in zeroProps)
+                            {
+                                _logger.Warn(
+                                    $"Similarity with 0 count. IncidentId {incidentId}, Name {property.Name}, Value: {prop.Value}");
+                                prop.Count = 1;
+                            }
+                        }
+
+                        var col = new SimilarityCollection(incidentId, reader.GetString(1));
+                        col.GetType().GetProperty("Id").SetValue(col, reader.GetInt32(0));
+                        foreach (var entity in properties)
+                        {
+                            var values = entity.Values
+                                .Select(x => new SimilarityValue(x.Value, x.Percentage, x.Count))
+                                .ToArray();
+                            var prop = new Similarity(entity.Name);
+                            prop.LoadValues(values);
+                            col.Properties.Add(prop);
+                        }
+                        collections.Add(col);
+                    }
+                }
+
+                return collections.Count == 0 ? null : new SimilaritiesReport(incidentId, collections);
+            }
+        }
+
+        public async Task UpdateAsync(SimilaritiesReport similarity)
+        {
+            await CreateAsync(similarity);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Modules/Tagging/DoInitialRun.cs b/src/Server/Coderr.Server.SqlServer/Modules/Tagging/DoInitialRun.cs
similarity index 89%
rename from src/Server/OneTrueError.SqlServer/Modules/Tagging/DoInitialRun.cs
rename to src/Server/Coderr.Server.SqlServer/Modules/Tagging/DoInitialRun.cs
index bf6aa4a8..1802ab29 100644
--- a/src/Server/OneTrueError.SqlServer/Modules/Tagging/DoInitialRun.cs
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Tagging/DoInitialRun.cs
@@ -1,96 +1,96 @@
-//using System;
-//using System.Linq;
-//using System.Threading.Tasks;
-//using DotNetCqs;
-//using Griffin.ApplicationServices;
-//using Griffin.Container;
-//using Griffin.Data;
-//using OneTrueError.Core.Api.Reports;
-//using OneTrueError.Core.Api.Reports.Queries;
-//using OneTrueError.Core.IncidentTagging.Data;
-//using OneTrueError.Data;
-
-//namespace OneTrueError.Core.IncidentTagging
-//{
-//    [Component(RegisterAsSelf = true)]
-//    public class DoInitialRun : IBackgroundJobAsync
-//    {
-//        private static bool _hasRun;
-//        private IAdoNetUnitOfWork _unitOfWork;
-//        private IQueryBus _queryBus;
-//        private ITagsRepository _repository;
-
-//        public DoInitialRun(IAdoNetUnitOfWork unitOfWork, IQueryBus queryBus, ITagsRepository repository)
-//        {
-//            _unitOfWork = unitOfWork;
-//            _queryBus = queryBus;
-//            _repository = repository;
-//        }
-
-//        public async Task ExecuteAsync()
-//        {
-//            if (_hasRun)
-//                return;
-//            _hasRun = true;
-
-//            using (var cmd = _unitOfWork.CreateCommand())
-//            {
-//                cmd.CommandText = "SELECT Value FROM Settings WHERE Name = 'TagRun'";
-//                if (cmd.ExecuteScalar() != null)
-//                    return;
-//            }
-
-//            using (var cmd = _unitOfWork.CreateCommand())
-//            {
-//                cmd.CommandText = "INSERT INTO Settings (Name, Value) VALUES('TagRun', @date)";
-//                cmd.AddParameter("date", DateTime.UtcNow.ToString());
-//                cmd.ExecuteNonQuery();
-//            }
-
-//            using (var cmd = _unitOfWork.CreateCommand())
-//            {
-//                cmd.CommandText = @"WITH summary AS (
-//    SELECT p.id as reportid, 
-//           p.incidentid, 
-//		   p.createdatutc,
-//           ROW_NUMBER() OVER(PARTITION BY p.incidentid 
-//                                 ORDER BY p.createdatutc asc) AS rk
-//      FROM errorreports p)
-//SELECT s.*
-//  FROM summary s
-//  left join incidenttags t on (t.incidentid=s.incidentid)
-// WHERE s.rk = 1
-// AND t.incidentid is null";
-//                using (var reader = cmd.ExecuteReader())
-//                {
-//                    while (reader.Read())
-//                    {
-//                        var reportId = (int) reader["reportid"];
-//                        var incidentId = (int)reader["incidentid"];
-//                        var q = new FindReport() {ReportId = reportId};
-//                        var report = await _queryBus.QueryAsync(q);
-
-//                        var dto = new NewReportDTO
-//                        {
-//                            ContextCollections = report.ContextCollections,
-//                            CreatedAtUtc = report.CreatedAtUtc,
-//                            Exception = report.Exception,
-//                            RemoteAddress = report.RemoteAddress,
-//                            ReportVersion = report.ReportVersion
-//                        };
-//                        var ctx = new TagIdentifierContext(dto);
-//                        var identiferProvider = new IdentifierProvider();
-//                        var identifiers = identiferProvider.GetIdentifiers(ctx);
-//                        foreach (var identifier in identifiers)
-//                        {
-//                            identifier.Identify(ctx);
-//                        }
-
-//                        _repository.Add(incidentId, ctx.Tags.ToArray());
-//                    }
-//                }
-//            }
-//        }
-//    }
-//}
-
+//using System;
+//using System.Linq;
+//using System.Threading.Tasks;
+//using DotNetCqs;
+//using Griffin.ApplicationServices;
+//using Coderr.Server.ReportAnalyzer.Abstractions;
+//using Griffin.Data;
+//using Coderr.Core.Api.Reports;
+//using Coderr.Core.Api.Reports.Queries;
+//using Coderr.Core.IncidentTagging.Data;
+//using Coderr.Data;
+
+//namespace Coderr.Core.IncidentTagging
+//{
+//    [Component(RegisterAsSelf = true)]
+//    public class DoInitialRun : IBackgroundJobAsync
+//    {
+//        private static bool _hasRun;
+//        private IAdoNetUnitOfWork _unitOfWork;
+//        private IQueryBus _queryBus;
+//        private ITagsRepository _repository;
+
+//        public DoInitialRun(IAdoNetUnitOfWork unitOfWork, IQueryBus queryBus, ITagsRepository repository)
+//        {
+//            _unitOfWork = unitOfWork;
+//            _queryBus = queryBus;
+//            _repository = repository;
+//        }
+
+//        public async Task HandleAsync(IMessageContext context, )
+//        {
+//            if (_hasRun)
+//                return;
+//            _hasRun = true;
+
+//            using (var cmd = _unitOfWork.CreateCommand())
+//            {
+//                cmd.CommandText = "SELECT Value FROM Settings WHERE Name = 'TagRun'";
+//                if (cmd.ExecuteScalar() != null)
+//                    return;
+//            }
+
+//            using (var cmd = _unitOfWork.CreateCommand())
+//            {
+//                cmd.CommandText = "INSERT INTO Settings (Name, Value) VALUES('TagRun', @date)";
+//                cmd.AddParameter("date", DateTime.UtcNow.ToString());
+//                cmd.ExecuteNonQuery();
+//            }
+
+//            using (var cmd = _unitOfWork.CreateCommand())
+//            {
+//                cmd.CommandText = @"WITH summary AS (
+//    SELECT p.id as reportid, 
+//           p.incidentid, 
+//		   p.createdatutc,
+//           ROW_NUMBER() OVER(PARTITION BY p.incidentid 
+//                                 ORDER BY p.createdatutc asc) AS rk
+//      FROM errorreports p)
+//SELECT s.*
+//  FROM summary s
+//  left join incidenttags t on (t.incidentid=s.incidentid)
+// WHERE s.rk = 1
+// AND t.incidentid is null";
+//                using (var reader = cmd.ExecuteReader())
+//                {
+//                    while (reader.Read())
+//                    {
+//                        var reportId = (int) reader["reportid"];
+//                        var incidentId = (int)reader["incidentid"];
+//                        var q = new FindReport() {ReportId = reportId};
+//                        var report = await _queryBus.QueryAsync(q);
+
+//                        var dto = new NewReportDTO
+//                        {
+//                            ContextCollections = report.ContextCollections,
+//                            CreatedAtUtc = report.CreatedAtUtc,
+//                            Exception = report.Exception,
+//                            RemoteAddress = report.RemoteAddress,
+//                            ReportVersion = report.ReportVersion
+//                        };
+//                        var ctx = new TagIdentifierContext(dto);
+//                        var identiferProvider = new IdentifierProvider();
+//                        var identifiers = identiferProvider.GetIdentifiers(ctx);
+//                        foreach (var identifier in identifiers)
+//                        {
+//                            identifier.Identify(ctx);
+//                        }
+
+//                        _repository.Add(incidentId, ctx.Tags.ToArray());
+//                    }
+//                }
+//            }
+//        }
+//    }
+//}
+
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Tagging/TagsRepository.cs b/src/Server/Coderr.Server.SqlServer/Modules/Tagging/TagsRepository.cs
new file mode 100644
index 00000000..dc081f6c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Tagging/TagsRepository.cs
@@ -0,0 +1,176 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Modules.Tags;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Modules.Tagging
+{
+    [ContainerService]
+    public class TagsRepository : ITagsRepository
+    {
+        private readonly IAdoNetUnitOfWork _adoNetUnitOfWork;
+
+        public TagsRepository(IAdoNetUnitOfWork adoNetUnitOfWork)
+        {
+            _adoNetUnitOfWork = adoNetUnitOfWork;
+        }
+
+        public async Task AddAsync(int incidentId, Tag[] tags)
+        {
+            foreach (var tag in tags)
+            {
+                using (var cmd = _adoNetUnitOfWork.CreateDbCommand())
+                {
+                    cmd.CommandText =
+                        "INSERT INTO IncidentTags (IncidentId, TagName, OrderNumber) VALUES(@incidentId, @name, @orderNumber)";
+                    cmd.AddParameter("incidentId", incidentId);
+                    cmd.AddParameter("name", tag.Name);
+                    cmd.AddParameter("orderNumber", tag.OrderNumber);
+                    await cmd.ExecuteNonQueryAsync();
+                }
+            }
+        }
+
+        public async Task UpdateTags(int incidentId, string[] tagsToAdd, string[] tagsToRemove)
+        {
+            foreach (var tag in tagsToAdd)
+            {
+                using (var cmd = _adoNetUnitOfWork.CreateDbCommand())
+                {
+                    cmd.CommandText =
+                        "INSERT INTO IncidentTags (IncidentId, TagName, OrderNumber) VALUES(@incidentId, @name, 1)";
+                    cmd.AddParameter("incidentId", incidentId);
+                    cmd.AddParameter("name", tag);
+                    await cmd.ExecuteNonQueryAsync();
+                }
+            }
+
+            foreach (var tag in tagsToRemove)
+            {
+                using (var cmd = _adoNetUnitOfWork.CreateDbCommand())
+                {
+                    cmd.CommandText =
+                        "DELETE FROM IncidentTags WHERE IncidentId = @incidentId AND TagName = @name";
+                    cmd.AddParameter("incidentId", incidentId);
+                    cmd.AddParameter("name", tag);
+                    await cmd.ExecuteNonQueryAsync();
+                }
+            }
+        }
+
+        public async Task AddTag(int incidentId, string tag)
+        {
+            using (var cmd = _adoNetUnitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "INSERT INTO IncidentTags (IncidentId, TagName, OrderNumber) VALUES(@incidentId, @name, 1)";
+                cmd.AddParameter("incidentId", incidentId);
+                cmd.AddParameter("name", tag);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task> GetIncidentTagsAsync(int incidentId)
+        {
+            using (var cmd = _adoNetUnitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = "SELECT * FROM IncidentTags WHERE IncidentId = @id ORDER BY OrderNumber";
+                cmd.AddParameter("id", incidentId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var tags = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var tag = new Tag((string)reader["TagName"], (int)reader["OrderNumber"]);
+                        tags.Add(tag);
+                    }
+                    return tags;
+                }
+            }
+        }
+
+        public async Task> GetTagsAsync(int? applicationId, int? incidentId)
+        {
+            using (var cmd = _adoNetUnitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"select TagName, min(OrderNumber)
+                                    FROM IncidentTags
+                                    INNER JOIN Incidents ON (IncidentTags.IncidentId=Incidents.Id)";
+                if (incidentId != null)
+                {
+                    cmd.CommandText += " WHERE Incidents.Id = @incidentId";
+                    cmd.AddParameter("@incidentId", incidentId.Value);
+                }
+                else if (applicationId != null)
+                {
+                    cmd.CommandText += " WHERE Incidents.ApplicationId = @applicationId";
+                    cmd.AddParameter("appId", applicationId.Value);
+                }
+
+                cmd.CommandText += " GROUP BY TagName";
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var tags = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var tag = new Tag((string)reader[0], (int)reader[1]);
+                        tags.Add(tag);
+                    }
+                    return tags;
+                }
+            }
+        }
+
+        public async Task> GetNewIncidentsForTag(int? applicationId, string tag)
+        {
+            using (var cmd = _adoNetUnitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"select it.IncidentId
+                                    from incidents i
+                                    join IncidentTags it ON (it.IncidentId = i.Id)
+                                    WHERE it.TagName = @tagName and i.State = 0";
+                if (applicationId != null)
+                {
+                    cmd.CommandText += " AND i.ApplicationId = @appId";
+                    cmd.AddParameter("appId", applicationId.Value);
+                }
+                cmd.AddParameter("tagName", tag);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var incidentIds = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        incidentIds.Add(reader.GetInt32(0));
+                    }
+                    return incidentIds;
+                }
+            }
+        }
+
+
+        public async Task> GetApplicationTagsAsync(int applicationId)
+        {
+            using (var cmd = _adoNetUnitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"select TagName, min(OrderNumber)
+                                    FROM IncidentTags
+                                    INNER JOIN Incidents ON (IncidentTags.IncidentId=Incidents.Id)
+                                    WHERE Incidents.ApplicationId = @id
+                                    GROUP BY TagName";
+                cmd.AddParameter("id", applicationId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var tags = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var tag = new Tag((string)reader[0], (int)reader[1]);
+                        tags.Add(tag);
+                    }
+                    return tags;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Triggers/AppTriggerRepository.cs b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/AppTriggerRepository.cs
new file mode 100644
index 00000000..bc0a85a5
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/AppTriggerRepository.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.App.Modules.Triggers;
+
+namespace Coderr.Server.SqlServer.Modules.Triggers
+{
+    [ContainerService]
+    class AppTriggerRepository : ITriggerRepository
+    {
+        public Task CreateAsync(Trigger trigger)
+        {
+            throw new NotImplementedException();
+        }
+
+        public Task DeleteAsync(int id)
+        {
+            throw new NotImplementedException();
+        }
+
+        public Task GetAsync(int id)
+        {
+            throw new NotImplementedException();
+        }
+
+        public IEnumerable GetForApplication(int applicationId)
+        {
+            throw new NotImplementedException();
+        }
+
+        public Task UpdateAsync(Trigger entity)
+        {
+            throw new NotImplementedException();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Triggers/CollectionMetadataMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/CollectionMetadataMapper.cs
new file mode 100644
index 00000000..418e847f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/CollectionMetadataMapper.cs
@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using Coderr.Server.ReportAnalyzer.Triggers;
+using Coderr.Server.SqlServer.Tools;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Triggers
+{
+    public class CollectionMetadataMapper : CrudEntityMapper
+    {
+        public CollectionMetadataMapper() : base("CollectionMetaData")
+        {
+            Property(x => x.IsUpdated).Ignore();
+
+            Property(x => x.Properties)
+                .ToPropertyValue(EntitySerializer.Deserialize>);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Triggers/DeleteTriggerHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/DeleteTriggerHandler.cs
new file mode 100644
index 00000000..20646d19
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/DeleteTriggerHandler.cs
@@ -0,0 +1,29 @@
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Triggers.Commands;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Modules.Triggers
+{
+    public class DeleteTriggerHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public DeleteTriggerHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context, DeleteTrigger command)
+        {
+            using (var cmd = (DbCommand) _uow.CreateCommand())
+            {
+                cmd.CommandText = "DELETE FROM Triggers WHERE Id = @id";
+                cmd.AddParameter("id", command.Id);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Modules/Triggers/GetContextCollectionMetadataHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/GetContextCollectionMetadataHandler.cs
similarity index 79%
rename from src/Server/OneTrueError.SqlServer/Modules/Triggers/GetContextCollectionMetadataHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Modules/Triggers/GetContextCollectionMetadataHandler.cs
index 4d03e388..0493bcf7 100644
--- a/src/Server/OneTrueError.SqlServer/Modules/Triggers/GetContextCollectionMetadataHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/GetContextCollectionMetadataHandler.cs
@@ -1,40 +1,39 @@
-using System.Data.Common;
-using System.Linq;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.Api.Modules.Triggers.Queries;
-
-namespace OneTrueError.SqlServer.Modules.Triggers
-{
-    [Component]
-    internal class GetContextCollectionMetadataHandler :
-        IQueryHandler
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public GetContextCollectionMetadataHandler(IAdoNetUnitOfWork unitOfWork)
-        {
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task ExecuteAsync(GetContextCollectionMetadata query)
-        {
-            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
-            {
-                cmd.CommandText =
-                    "SELECT * FROM CollectionMetadata WHERE ApplicationId = @id";
-
-                cmd.AddParameter("id", query.ApplicationId);
-                var items = await cmd.ToListAsync(new CollectionMetadataMapper());
-                return items.Select(x => new GetContextCollectionMetadataItem
-                {
-                    Name = x.Name,
-                    Properties = x.Properties.ToArray()
-                }).ToArray();
-            }
-        }
-    }
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Triggers.Queries;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Triggers
+{
+    internal class GetContextCollectionMetadataHandler :
+        IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetContextCollectionMetadataHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetContextCollectionMetadata query)
+        {
+            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "SELECT * FROM CollectionMetadata WHERE ApplicationId = @id";
+
+                cmd.AddParameter("id", query.ApplicationId);
+                var items = await cmd.ToListAsync(new CollectionMetadataMapper());
+                return items.Select(x => new GetContextCollectionMetadataItem
+                {
+                    Name = x.Name,
+                    Properties = x.Properties.ToArray()
+                }).ToArray();
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Triggers/GetTriggersForApplicationHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/GetTriggersForApplicationHandler.cs
new file mode 100644
index 00000000..e7be9f29
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/GetTriggersForApplicationHandler.cs
@@ -0,0 +1,32 @@
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Triggers;
+using Coderr.Server.Api.Modules.Triggers.Queries;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Triggers
+{
+    public class GetTriggersForApplicationHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetTriggersForApplicationHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetTriggersForApplication query)
+        {
+            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = "SELECT * FROM Triggers WHERE [ApplicationId]=@appId";
+                cmd.AddParameter("appId", query.ApplicationId);
+                return (await cmd.ToListAsync()).ToArray();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Triggers/ReportAnalyzerTriggerRepository.cs b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/ReportAnalyzerTriggerRepository.cs
new file mode 100644
index 00000000..af0d189a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/ReportAnalyzerTriggerRepository.cs
@@ -0,0 +1,142 @@
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.ReportAnalyzer.Triggers;
+using Coderr.Server.SqlServer.Tools;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Triggers
+{
+    [ContainerService]
+    public class ReportAnalyzerTriggerRepository : ITriggerRepository
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public ReportAnalyzerTriggerRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+
+        public async Task> GetCollectionsAsync(int applicationId)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "SELECT * FROM CollectionMetadata WHERE ApplicationId = @id";
+
+                cmd.AddParameter("id", applicationId);
+                return await cmd.ToListAsync(new CollectionMetadataMapper());
+            }
+        }
+
+
+        public async Task UpdateAsync(CollectionMetadata collection)
+        {
+            var props = EntitySerializer.Serialize(collection.Properties);
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"UPDATE CollectionMetadata SET Properties = @Properties
+                                    WHERE Id = @id";
+
+                cmd.AddParameter("Id", collection.Id);
+                cmd.AddParameter("Properties", props);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task CreateAsync(CollectionMetadata entity)
+        {
+            var props = EntitySerializer.Serialize(entity.Properties);
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    @"INSERT INTO CollectionMetadata (Name, ApplicationId, Properties) VALUES(@Name, @ApplicationId, @Properties)";
+                cmd.AddParameter("Name", entity.Name);
+                cmd.AddParameter("ApplicationId", entity.ApplicationId);
+                cmd.AddParameter("Properties", props);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+
+        public async Task CreateAsync(Trigger trigger)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "INSERT INTO Triggers (ApplicationId, Name, Description, Rules, Actions, LastTriggerAction, RunForNewIncidents, RunForExistingIncidents) " +
+                    "VALUES(@ApplicationId, @Name, @Description, @Rules, @Actions, @LastTriggerAction, @RunForNewIncidents, @RunForExistingIncidents)";
+                cmd.AddParameter("ApplicationId", trigger.ApplicationId);
+                cmd.AddParameter("Name", trigger.Name);
+                cmd.AddParameter("Description", trigger.Description);
+                cmd.AddParameter("Rules", EntitySerializer.Serialize(trigger.Rules));
+                cmd.AddParameter("Actions", EntitySerializer.Serialize(trigger.Actions));
+                cmd.AddParameter("LastTriggerAction", trigger.LastTriggerAction);
+                cmd.AddParameter("RunForNewIncidents", trigger.RunForNewIncidents);
+                cmd.AddParameter("RunForExistingIncidents", trigger.RunForExistingIncidents);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public IEnumerable GetForApplication(int applicationId)
+        {
+            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "SELECT * FROM Triggers WHERE ApplicationId = @applicationId";
+                cmd.AddParameter("applicationId", applicationId);
+                return cmd.ToList(new TriggerMapper()).ToList();
+            }
+        }
+
+
+        public async Task GetAsync(int id)
+        {
+            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "SELECT * FROM Triggers WHERE Id = @id";
+                cmd.AddParameter("id", id);
+                return await cmd.FirstAsync(new TriggerMapper());
+            }
+        }
+
+        public async Task UpdateAsync(Trigger trigger)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "UPDATE Triggers SET Name=@Name, Description=@Description, Rules=@Rules, Actions=@Actions, LastTriggerAction=@LastTriggerAction, RunForNewIncidents = @RunForNewIncidents, RunForExistingIncidents=@RunForExistingIncidents " +
+                    " WHERE Id=@Id";
+                cmd.AddParameter("Id", trigger.Id);
+                cmd.AddParameter("Name", trigger.Name);
+                cmd.AddParameter("Description", trigger.Description);
+                cmd.AddParameter("Rules", EntitySerializer.Serialize(trigger.Rules));
+                cmd.AddParameter("Actions", EntitySerializer.Serialize(trigger.Actions));
+                cmd.AddParameter("LastTriggerAction", trigger.LastTriggerAction);
+                cmd.AddParameter("RunForNewIncidents", trigger.RunForNewIncidents);
+                cmd.AddParameter("RunForExistingIncidents", trigger.RunForExistingIncidents);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+        public async Task DeleteAsync(int id)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    "DELETE FROM Triggers" +
+                    " WHERE Id=@Id";
+                cmd.AddParameter("Id", id);
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Triggers/TriggerDtoMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/TriggerDtoMapper.cs
new file mode 100644
index 00000000..0821e29a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/TriggerDtoMapper.cs
@@ -0,0 +1,17 @@
+using Coderr.Server.Api.Modules.Triggers;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Triggers
+{
+    public class TriggerDtoMapper : EntityMapper
+    {
+        public TriggerDtoMapper()
+        {
+            Property(x => x.Id)
+                .ToPropertyValue(x => x.ToString());
+
+            Property(x => x.Summary)
+                .Ignore();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/TriggerMapper.cs
similarity index 87%
rename from src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerMapper.cs
rename to src/Server/Coderr.Server.SqlServer/Modules/Triggers/TriggerMapper.cs
index 624c9817..2b45faa6 100644
--- a/src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerMapper.cs
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Triggers/TriggerMapper.cs
@@ -1,36 +1,36 @@
-using System;
-using System.Collections.Generic;
-using Griffin.Data.Mapper;
-using OneTrueError.App.Modules.Triggers.Domain;
-using OneTrueError.SqlServer.Tools;
-
-namespace OneTrueError.SqlServer.Modules.Triggers
-{
-    public class TriggerMapper : CrudEntityMapper
-    {
-        public TriggerMapper() : base("Triggers")
-        {
-            Property(x => x.LastTriggerAction)
-                .ToPropertyValue(o => (LastTriggerAction) Enum.Parse(typeof(LastTriggerAction), o.ToString()))
-                .ToColumnValue(o => o.ToString());
-
-
-            Property(x => x.Actions)
-                .ToPropertyValue(o => EntitySerializer.Deserialize>((string) o))
-                .ToColumnValue(o => o.ToString());
-
-            Property(x => x.Rules)
-                .ToPropertyValue(o => EntitySerializer.Deserialize>((string) o))
-                .ToColumnValue(o => o.ToString());
-
-            Property(x => x.RunForExistingIncidents)
-                .ToPropertyValue(Convert.ToBoolean);
-
-            Property(x => x.RunForNewIncidents)
-                .ToPropertyValue(Convert.ToBoolean);
-
-            Property(x => x.RunForReopenedIncidents)
-                .ToPropertyValue(Convert.ToBoolean);
-        }
-    }
+using System;
+using System.Collections.Generic;
+using Coderr.Server.ReportAnalyzer.Triggers;
+using Coderr.Server.SqlServer.Tools;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Triggers
+{
+    public class TriggerMapper : CrudEntityMapper
+    {
+        public TriggerMapper() : base("Triggers")
+        {
+            Property(x => x.LastTriggerAction)
+                .ToPropertyValue(o => (LastTriggerAction) Enum.Parse(typeof(LastTriggerAction), o.ToString()))
+                .ToColumnValue(o => o.ToString());
+
+
+            Property(x => x.Actions)
+                .ToPropertyValue(o => EntitySerializer.Deserialize>((string) o))
+                .ToColumnValue(o => o.ToString());
+
+            Property(x => x.Rules)
+                .ToPropertyValue(o => EntitySerializer.Deserialize>((string) o))
+                .ToColumnValue(o => o.ToString());
+
+            Property(x => x.RunForExistingIncidents)
+                .ToPropertyValue(Convert.ToBoolean);
+
+            Property(x => x.RunForNewIncidents)
+                .ToPropertyValue(Convert.ToBoolean);
+
+            Property(x => x.RunForReopenedIncidents)
+                .ToPropertyValue(Convert.ToBoolean);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionConfig.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionConfig.cs
new file mode 100644
index 00000000..c036debc
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionConfig.cs
@@ -0,0 +1,9 @@
+namespace Coderr.Server.SqlServer.Modules.Versions
+{
+    public class ApplicationVersionConfig
+    {
+        public int Id { get; set; }
+        public int ApplicationId { get; set; }
+        public string AssemblyName { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionConfigMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionConfigMapper.cs
new file mode 100644
index 00000000..a6789eaa
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionConfigMapper.cs
@@ -0,0 +1,13 @@
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Versions
+{
+    public class ApplicationVersionConfigMapper : CrudEntityMapper
+    {
+        public ApplicationVersionConfigMapper() : base("ApplicationVersions")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionMapper.cs
new file mode 100644
index 00000000..1347b041
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionMapper.cs
@@ -0,0 +1,19 @@
+using Coderr.Server.App.Modules.Versions;
+using Coderr.Server.Domain.Modules.ApplicationVersions;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Versions
+{
+    public class ApplicationVersionMapper : CrudEntityMapper
+    {
+        public ApplicationVersionMapper() : base("ApplicationVersions")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+            Property(x => x.ReceivedFirstReportAtUtc)
+                .ColumnName("FirstReportDate");
+            Property(x => x.ReceivedLastReportAtUtc)
+                .ColumnName("LastReportDate");
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionMonthMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionMonthMapper.cs
new file mode 100644
index 00000000..a06d2b7f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/ApplicationVersionMonthMapper.cs
@@ -0,0 +1,15 @@
+using Coderr.Server.App.Modules.Versions;
+using Coderr.Server.Domain.Modules.ApplicationVersions;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Versions
+{
+    public class ApplicationVersionMonthMapper : CrudEntityMapper
+    {
+        public ApplicationVersionMonthMapper() : base("ApplicationVersionMonths")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/DeleteAbandonedFeedback.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/DeleteAbandonedFeedback.cs
new file mode 100644
index 00000000..4cc6c083
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/DeleteAbandonedFeedback.cs
@@ -0,0 +1,30 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Modules.Versions
+{
+    [ContainerService(RegisterAsSelf = true)]
+    internal class DeleteAbandonedVersions : IBackgroundJobAsync
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public DeleteAbandonedVersions(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"DELETE FROM IncidentVersions
+                                    FROM IncidentVersions
+                                    LEFT JOIN Incidents ON (Incidents.Id = IncidentId)
+                                    WHERE Incidents.Id IS NULL";
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/GetApplicationVersionsHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/GetApplicationVersionsHandler.cs
new file mode 100644
index 00000000..06cf9bd2
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/GetApplicationVersionsHandler.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Versions.Queries;
+using Coderr.Server.Infrastructure;
+using DotNetCqs;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Modules.Versions.Queries
+{
+    internal class GetApplicationVersionsHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public GetApplicationVersionsHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context,
+            GetApplicationVersions query)
+        {
+            var sql =
+                @"SELECT version, sum(incidentcount) incidentcount, sum(reportcount) reportcount, min(FirstReportDate) as FirstReportDate, max(LastReportDate)as LastReportDate
+  FROM ApplicationVersions WITH (NoLock)
+  join ApplicationVersionMonths WITH (NoLock) on (versionid=applicationversions.id)
+  where applicationid=@appId
+  group by version
+  order by version
+";
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText = sql;
+                cmd.AddParameter("appId", query.ApplicationId);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var items = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        var item = new GetApplicationVersionsResultItem
+                        {
+                            Version = reader[0].ToString(),
+                            FirstReportReceivedAtUtc = (DateTime) reader[3],
+                            LastReportReceivedAtUtc = (DateTime) reader[4],
+                            IncidentCount = (int) reader[1],
+                            ReportCount = (int) reader[2]
+                        };
+                        items.Add(item);
+                    }
+
+                    var comparer = new ApplicationVersionComparer();
+                    var sortedItems = items.OrderByDescending(x => x.Version, comparer).ToArray();
+                    return new GetApplicationVersionsResult {Items = sortedItems};
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/GetVersionHistoryHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/GetVersionHistoryHandler.cs
new file mode 100644
index 00000000..00aad139
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/GetVersionHistoryHandler.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Versions.Queries;
+using DotNetCqs;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Modules.Versions.Queries
+{
+    public class GetVersionHistoryHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetVersionHistoryHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetVersionHistory query)
+        {
+            var sql =
+                @"select avm.YearMonth, avm.IncidentCount, avm.ReportCount, LastUpdateAtUtc, av.Version
+                  from [ApplicationVersionMonths] avm
+                  JOIN ApplicationVersions av ON (av.Id=avm.VersionId)
+                  WHERE av.ApplicationId = @appId
+                  ORDER BY YearMonth, ApplicationId, av.Version";
+
+            var first = DateTime.MinValue;
+            var last = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.AddParameter("appId", query.ApplicationId);
+                if (query.FromDate != null)
+                {
+                    sql += " AND YearMonth >= @from";
+                    cmd.AddParameter("from", query.FromDate.Value);
+                    first = query.FromDate.Value;
+                }
+
+                if (query.ToDate != null)
+                {
+                    sql += " AND YearMonth <= @to";
+                    cmd.AddParameter("to", query.ToDate.Value);
+                    last = query.ToDate.Value;
+                }
+
+                cmd.CommandText = sql;
+                var versions = new Versions();
+
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var month = (DateTime) reader["YearMonth"];
+                        if (first == DateTime.MinValue)
+                            first = month;
+                        var incindentCount = (int) reader["IncidentCount"];
+                        var reportCount = (int) reader["ReportCount"];
+                        var version = (string) reader["Version"];
+
+                        versions.AddCounts(version, month, incindentCount, reportCount);
+                    }
+                }
+
+                if (versions.IsEmpty)
+                    return new GetVersionHistoryResult
+                    {
+                        Dates = new string[0],
+                        IncidentCounts = new GetVersionHistoryResultSet[0],
+                        ReportCounts = new GetVersionHistoryResultSet[0]
+                    };
+
+
+                versions.PadMonths(first, last);
+
+                var result = new GetVersionHistoryResult
+                {
+                    Dates = versions.GetDates(),
+                    IncidentCounts = versions.BuildIncidentSeries(),
+                    ReportCounts = versions.BuildReportSeries()
+                };
+                return result;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/VersionRange.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/VersionRange.cs
new file mode 100644
index 00000000..5480335a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/VersionRange.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+
+namespace Coderr.Server.SqlServer.Modules.Versions.Queries
+{
+    internal class VersionRange
+    {
+        private readonly List _incdentRange = new List();
+        private readonly Dictionary _incidents = new Dictionary();
+        private readonly List _reportRange = new List();
+        private readonly Dictionary _reports = new Dictionary();
+        private List _dates = new List();
+        public IEnumerable IncidentSeries => _incdentRange;
+
+        public IEnumerable ReportSeries => _reportRange;
+
+        public IEnumerable Dates => _dates;
+        public string Version { get; set; }
+
+        public void AddCounts(DateTime yearMonth, int incidentCount, int reportCount)
+        {
+            _reports[yearMonth] = reportCount;
+            _incidents[yearMonth] = incidentCount;
+        }
+
+        public void EnsureMonth(DateTime yearMonth)
+        {
+            _dates.Add(yearMonth);
+            if (!_incidents.TryGetValue(yearMonth, out var count))
+                _incdentRange.Add(0);
+            else
+                _incdentRange.Add(count);
+
+            if (!_reports.TryGetValue(yearMonth, out var count1))
+                _reportRange.Add(0);
+            else
+                _reportRange.Add(count1);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/Versions.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/Versions.cs
new file mode 100644
index 00000000..144b6bdb
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/Queries/Versions.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Coderr.Server.Api.Modules.Versions.Queries;
+using Coderr.Server.Infrastructure;
+
+namespace Coderr.Server.SqlServer.Modules.Versions.Queries
+{
+    internal class Versions
+    {
+        private readonly Dictionary _versions = new Dictionary();
+
+        public bool IsEmpty => _versions.Count == 0;
+
+        public void AddCounts(string version, DateTime yearMonth, int incidentCount, int reportCount)
+        {
+            if (!_versions.TryGetValue(version, out var entity))
+            {
+                entity = new VersionRange();
+                _versions[version] = entity;
+            }
+
+            entity.AddCounts(yearMonth, incidentCount, reportCount);
+        }
+
+        public GetVersionHistoryResultSet[] BuildIncidentSeries()
+        {
+            var items = new List();
+            foreach (var key in _versions.Keys)
+            {
+                items.Add(new GetVersionHistoryResultSet
+                {
+                    Name = key,
+                    Values = _versions[key].IncidentSeries.ToArray()
+                });
+            }
+
+            return items.OrderBy(x => x.Name, new ApplicationVersionComparer()).ToArray();
+        }
+
+        public GetVersionHistoryResultSet[] BuildReportSeries()
+        {
+            var items = new List();
+            foreach (var key in _versions.Keys)
+            {
+                items.Add(new GetVersionHistoryResultSet
+                {
+                    Name = key,
+                    Values = _versions[key].ReportSeries.ToArray()
+                });
+            }
+
+            return items.OrderBy(x => x.Name, new ApplicationVersionComparer()).ToArray();
+        }
+
+        public string[] GetDates()
+        {
+            var firstKey = _versions.Keys.First();
+            return _versions[firstKey].Dates.Select(x => x.ToShortDateString()).ToArray();
+        }
+
+        public void PadMonths(DateTime from, DateTime to)
+        {
+            foreach (var version in _versions)
+            {
+                var current = from;
+                while (current <= to)
+                {
+                    version.Value.EnsureMonth(current);
+                    current = current.AddMonths(1);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Versions/VersionRepository.cs b/src/Server/Coderr.Server.SqlServer/Modules/Versions/VersionRepository.cs
new file mode 100644
index 00000000..e9aacdb0
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Versions/VersionRepository.cs
@@ -0,0 +1,136 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Modules.ApplicationVersions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Versions
+{
+    /// 
+    ///     ADO.NET based implementation of .
+    /// 
+    [ContainerService]
+    public class VersionRepository : IApplicationVersionRepository
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        /// 
+        ///     Creates a new instance of 
+        /// 
+        /// Unit of work
+        public VersionRepository(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow ?? throw new ArgumentNullException(nameof(uow));
+        }
+
+        /// 
+        public async Task CreateAsync(ApplicationVersionMonth month)
+        {
+            if (month == null) throw new ArgumentNullException(nameof(month));
+
+            await _uow.InsertAsync(month);
+        }
+
+        /// 
+        public async Task CreateAsync(ApplicationVersion entity)
+        {
+            if (entity == null) throw new ArgumentNullException(nameof(entity));
+
+            await _uow.InsertAsync(entity);
+        }
+
+
+        /// 
+        public Task FindMonthForApplicationAsync(int versionId, int year, int month)
+        {
+            if (versionId <= 0) throw new ArgumentOutOfRangeException(nameof(versionId));
+            if (year <= 0) throw new ArgumentOutOfRangeException(nameof(year));
+            if (month <= 0) throw new ArgumentOutOfRangeException(nameof(month));
+
+            var date = new DateTime(year, month, 1);
+            return _uow.FirstOrDefaultAsync(new {VersionId = versionId, YearMonth = date});
+        }
+
+        /// 
+        public Task FindVersionAsync(int applicationId, string version)
+        {
+            if (version == null) throw new ArgumentNullException(nameof(version));
+            if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId));
+
+            return _uow.FirstOrDefaultAsync(new {ApplicationId = applicationId, Version = version});
+        }
+
+        public async Task> FindForIncidentAsync(int incidentId)
+        {
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText = @"select ApplicationVersions.*
+                                      FROM IncidentVersions
+                                      JOIN ApplicationVersions ON (IncidentVersions.VersionId = ApplicationVersions.Id)
+                                      WHERE IncidentVersions.IncidentId = @incidentId";
+                cmd.AddParameter("incidentId", incidentId);
+                return await cmd.ToListAsync();
+            }
+        }
+
+        public void SaveIncidentVersion(int incidentId, int versionId)
+        {
+            var sql = @"INSERT INTO IncidentVersions (IncidentId, VersionId)
+                        SELECT @incidentId, @versionId
+                        WHERE NOT EXISTS (
+                            select IncidentId
+                              from IncidentVersions 
+                              WHERE VersionId=@versionId AND IncidentId = @incidentId
+                        )";
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText = sql;
+                cmd.AddParameter("incidentId", incidentId);
+                cmd.AddParameter("versionId", versionId);
+                cmd.ExecuteNonQuery();
+            }
+        }
+
+        public async Task UpdateAsync(ApplicationVersionMonth month)
+        {
+            await _uow.UpdateAsync(month);
+        }
+
+        public async Task UpdateAsync(ApplicationVersion entity)
+        {
+            await _uow.UpdateAsync(entity);
+        }
+
+        public async Task> FindVersionsAsync(int appId)
+        {
+            return await _uow.ToListAsync(new StringMapper(), "SELECT Version FROM ApplicationVersions WHERE ApplicationId = @id",
+                new {id = appId});
+        }
+
+
+        public async Task GetVersionAssemblyNameAsync(int applicationId)
+        {
+            var item = await _uow.FirstOrDefaultAsync(new {ApplicationId = applicationId});
+            return item?.AssemblyName;
+        }
+    }
+
+    public class StringMapper : IEntityMapper
+    {
+        public void Map(IDataRecord source, string destination)
+        {
+        }
+
+        public object Create(IDataRecord record)
+        {
+            return record[0];
+        }
+
+        public void Map(IDataRecord source, object destination)
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/AddEntryHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/AddEntryHandler.cs
new file mode 100644
index 00000000..80cdb17c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/AddEntryHandler.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Whitelists.Commands;
+using Coderr.Server.App.Modules.Whitelists;
+using DnsClient;
+using DnsClient.Protocol;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    internal class AddEntryHandler : IMessageHandler
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(AddEntryHandler));
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public AddEntryHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context, AddEntry message)
+        {
+            var entry = new Whitelist {DomainName = message.DomainName};
+            await _uow.InsertAsync(entry);
+
+            foreach (var dto in message.ApplicationIds)
+            {
+                var entity = new WhitelistedDomainApplication {DomainId = entry.Id, ApplicationId = dto};
+                await _uow.InsertAsync(entity);
+            }
+
+            if (message.IpAddresses?.Length > 0)
+            {
+                foreach (var ip in message.IpAddresses)
+                {
+                    var entity = new WhitelistedDomainIp
+                    {
+                        DomainId = entry.Id,
+                        IpType = IpType.Manual,
+                        IpAddress = IPAddress.Parse(ip),
+                        StoredAtUtc = DateTime.UtcNow
+                    };
+
+                    await _uow.InsertAsync(entity);
+                }
+            }
+            else
+                await LookupIps(message, entry);
+        }
+
+        private async Task LookupIps(AddEntry message, Whitelist entry)
+        {
+            var lookup = new LookupClient();
+            var result = await lookup.QueryAsync(message.DomainName, QueryType.A);
+            var result2 = await lookup.QueryAsync(message.DomainName, QueryType.AAAA);
+
+            var results = result.AllRecords.ToList();
+            results.AddRange(result2.AllRecords);
+
+            foreach (var record in results)
+            {
+                switch (record)
+                {
+                    case ARecord ipRecord:
+                        await _uow.InsertAsync(new WhitelistedDomainIp
+                        {
+                            DomainId = entry.Id,
+                            IpAddress = ipRecord.Address,
+                            IpType = IpType.Lookup,
+                            StoredAtUtc = DateTime.UtcNow
+                        });
+                        break;
+                    case AaaaRecord ip6Record:
+                        await _uow.InsertAsync(new WhitelistedDomainIp
+                        {
+                            DomainId = entry.Id,
+                            IpAddress = ip6Record.Address,
+                            IpType = IpType.Lookup,
+                            StoredAtUtc = DateTime.UtcNow
+                        });
+                        break;
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/EditEntryHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/EditEntryHandler.cs
new file mode 100644
index 00000000..af4f0b59
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/EditEntryHandler.cs
@@ -0,0 +1,169 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Whitelists.Commands;
+using Coderr.Server.App.Modules.Whitelists;
+using DnsClient;
+using DnsClient.Protocol;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    internal class EditEntryHandler : IMessageHandler
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(EditEntryHandler));
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public EditEntryHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context, EditEntry message)
+        {
+            var entry = await _uow.FirstAsync(new { message.Id });
+
+            await FetchIps(entry);
+            await FetchApplications(entry);
+
+            await UpdateApplications(message, entry);
+            await UpdateIps(message, entry);
+        }
+
+        private async Task FetchIps(Whitelist entry)
+        {
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    $"SELECT * FROM  WhitelistedDomainIps WHERE DomainId = @domainId";
+                cmd.AddParameter("domainId", entry.Id);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var addresses = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        addresses.Add(new WhitelistedDomainIp()
+                        {
+                            DomainId = entry.Id,
+                            IpAddress = IPAddress.Parse((string) reader["IpAddress"]),
+                            IpType = (IpType) (int) reader["IpType"],
+                            StoredAtUtc = (DateTime) reader["StoredAtUtc"]
+                        });
+                    }
+
+                    entry.IpAddresses = addresses.ToArray();
+                }
+            }
+        }
+
+        private async Task FetchApplications(Whitelist entry)
+        {
+            using (var cmd = _uow.CreateDbCommand())
+            {
+                cmd.CommandText =
+                    $"SELECT * FROM  WhitelistedDomainApps WHERE DomainId = @domainId";
+                cmd.AddParameter("domainId", entry.Id);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var apps = new List();
+                    while (await reader.ReadAsync())
+                    {
+                        apps.Add((int) reader["ApplicationId"]);
+                    }
+
+                    entry.ApplicationIds = apps.ToArray();
+                }
+            }
+        }
+
+        private async Task LookupIps(EditEntry message, Whitelist whitelist)
+        {
+            var lookup = new LookupClient();
+            var result = await lookup.QueryAsync(whitelist.DomainName, QueryType.ANY);
+            foreach (var record in result.AllRecords)
+            {
+                switch (record)
+                {
+                    case ARecord ipRecord:
+                        await _uow.InsertAsync(new WhitelistedDomainIp
+                        {
+                            DomainId = whitelist.Id,
+                            IpAddress = ipRecord.Address,
+                            IpType = IpType.Lookup,
+                            StoredAtUtc = DateTime.UtcNow
+                        });
+                        break;
+                    case AaaaRecord ip6Record:
+                        await _uow.InsertAsync(new WhitelistedDomainIp
+                        {
+                            DomainId = whitelist.Id,
+                            IpAddress = ip6Record.Address,
+                            IpType = IpType.Lookup,
+                            StoredAtUtc = DateTime.UtcNow
+                        });
+                        break;
+                }
+            }
+        }
+
+        private async Task UpdateApplications(EditEntry message, Whitelist entry)
+        {
+            var dbApps = await _uow.ToListAsync("DomainId = @id", new { id = message.Id });
+
+            //find new
+            var newApps = message.ApplicationIds.Except(dbApps.Select(x => x.ApplicationId));
+            foreach (var newApp in newApps)
+            {
+                var entity = new WhitelistedDomainApplication { DomainId = entry.Id, ApplicationId = newApp };
+                await _uow.InsertAsync(entity);
+            }
+
+            //find removed
+            var removedApps = dbApps.Select(x => x.ApplicationId)
+                .Except(message.ApplicationIds)
+                .Select(x => dbApps.First(y => x == y.ApplicationId));
+            foreach (var app in removedApps) await _uow.DeleteAsync(app);
+        }
+
+        private async Task UpdateIps(EditEntry message, Whitelist whitelist)
+        {
+            var dbIps = await _uow.ToListAsync("DomainId = @id", new { id = message.Id });
+
+            foreach (var address in message.IpAddresses)
+            {
+                var dbIp = dbIps.FirstOrDefault(x => x.IpAddress.ToString() == address);
+                if (dbIp == null)
+                {
+                    var entity = new WhitelistedDomainIp
+                    {
+                        DomainId = whitelist.Id,
+                        IpType = IpType.Manual,
+                        IpAddress = IPAddress.Parse(address),
+                        StoredAtUtc = DateTime.UtcNow
+                    };
+                    await _uow.InsertAsync(entity);
+                    continue;
+                }
+
+                if (dbIp.IpType == IpType.Manual)
+                    continue;
+
+                dbIp.IpType = IpType.Manual;
+                await _uow.UpdateAsync(dbIp);
+
+            }
+
+            //find removed
+            var removedEntries = dbIps.Select(x => x.IpAddress.ToString())
+                .Except(message.IpAddresses)
+                .Select(x => dbIps.First(y => x == y.IpAddress.ToString()));
+            foreach (var entry in removedEntries)
+                await _uow.DeleteAsync(entry);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/GetWhitelistEntriesHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/GetWhitelistEntriesHandler.cs
new file mode 100644
index 00000000..fd5b115c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/GetWhitelistEntriesHandler.cs
@@ -0,0 +1,144 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Whitelists.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    public class GetWhitelistEntriesHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private readonly IEntityMapper _mapper =
+            new MirrorMapper();
+
+
+        public GetWhitelistEntriesHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetWhitelistEntries message)
+        {
+            var items = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                CreateSqlStatement(message, cmd);
+
+                var appMap = new Dictionary>();
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        MapRow(reader, items, appMap);
+                    }
+
+                    foreach (var map in appMap)
+                    {
+                        items.First(x => x.Id == map.Key).Applications = map.Value.ToArray();
+                    }
+                }
+            }
+
+            if (!items.Any())
+                return new GetWhitelistEntriesResult { Entries = new GetWhitelistEntriesResultItem[0] };
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                var entryIds = items.Select(x => x.Id).ToArray();
+                cmd.CommandText =
+                    $"SELECT * FROM  WhitelistedDomainIps WHERE DomainId IN ({string.Join(", ", entryIds)})";
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var map = new Dictionary>();
+                    while (await reader.ReadAsync())
+                    {
+                        var domainId = (int)reader["DomainId"];
+                        if (!map.TryGetValue(domainId, out var ipItems))
+                        {
+                            ipItems = new List();
+                            map[domainId] = ipItems;
+                        }
+                        ipItems.Add(new GetWhitelistEntriesResultItemIp
+                        {
+                            Address = (string)reader["IpAddress"],
+                            Type = (ResultItemIpType)(int)reader["IpType"],
+                            UpdatedAtUtc = (DateTime)reader["StoredAtUtc"]
+                        });
+                    }
+
+                    foreach (var kvp in map)
+                    {
+                        items.First(x => x.Id == kvp.Key).IpAddresses = kvp.Value.ToArray();
+                    }
+                }
+            }
+
+            return new GetWhitelistEntriesResult { Entries = items.ToArray() };
+        }
+
+        private static void CreateSqlStatement(GetWhitelistEntries message, DbCommand cmd)
+        {
+            cmd.CommandText =
+                @"SELECT WhitelistedDomains.*, WhitelistedDomainApps.ApplicationId, Applications.Name ApplicationName
+                            FROM WhitelistedDomains
+                            LEFT JOIN WhitelistedDomainApps ON (DomainId = WhitelistedDomains.Id)
+                            LEFT JOIN Applications ON (ApplicationId = Applications.Id)";
+            if (message.ApplicationId != null && !string.IsNullOrWhiteSpace(message.DomainName))
+            {
+                cmd.CommandText += @"
+                            WHERE ApplicationId = @applicationId 
+                            AND DomainName = @domainName";
+                cmd.AddParameter("domainName", message.DomainName);
+                cmd.AddParameter("applicationId", message.ApplicationId.Value);
+            }
+            else if (message.ApplicationId != null)
+            {
+                cmd.CommandText += @"
+                            WHERE ApplicationId = @applicationId";
+                cmd.AddParameter("domainName", message.DomainName);
+                cmd.AddParameter("applicationId", message.ApplicationId.Value);
+            }
+            else if (!string.IsNullOrEmpty(message.DomainName))
+            {
+                cmd.CommandText += @"
+                            WHERE DomainName = @domainName";
+                cmd.AddParameter("domainName", message.DomainName);
+            }
+        }
+
+        private static void MapRow(IDataRecord record, ICollection items,
+            IDictionary> appMap)
+        {
+            var item = items.FirstOrDefault(x => x.Id == record.GetInt32(0));
+            if (item == null)
+            {
+                item = new GetWhitelistEntriesResultItem
+                {
+                    Id = record.GetInt32(0),
+                    DomainName = (string)record["DomainName"]
+                };
+                items.Add(item);
+                appMap[item.Id] = new List();
+            }
+
+            var appIdValue = record["ApplicationId"];
+            if (appIdValue is DBNull)
+            {
+                return;
+            }
+
+            var app = new GetWhitelistEntriesResultItemApp
+            {
+                ApplicationId = (int)appIdValue,
+                Name = (string)record["ApplicationName"]
+            };
+            appMap[item.Id].Add(app);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/RemoveDomainHandler.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/RemoveDomainHandler.cs
new file mode 100644
index 00000000..05154f53
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/RemoveDomainHandler.cs
@@ -0,0 +1,27 @@
+using System.Threading.Tasks;
+using Coderr.Server.Api.Modules.Whitelists.Commands;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    internal class RemoveDomainHandler : IMessageHandler
+    {
+        private readonly IAdoNetUnitOfWork _uow;
+
+        public RemoveDomainHandler(IAdoNetUnitOfWork uow)
+        {
+            _uow = uow;
+        }
+
+        public async Task HandleAsync(IMessageContext context, RemoveEntry message)
+        {
+            var item = await _uow.FirstOrDefaultAsync("Id = @id", new {message.Id});
+            if (item == null)
+                return;
+
+            await _uow.DeleteAsync(item);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistRepository.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistRepository.cs
new file mode 100644
index 00000000..3ec3cfeb
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistRepository.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.App.Modules.Whitelists;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    [ContainerService]
+    public class WhitelistRepository : IWhitelistRepository
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public WhitelistRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task FindIp(int applicationId, IPAddress address)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                // ORDER BY appId desc = get specific one first, then the generic one.
+                cmd.CommandText = @"select ip.*
+                                    from WhitelistedDomains d
+                                    left join WhitelistedDomainIps ip ON (d.Id = ip.DomainId)
+                                    left join WhitelistedDomainApps app ON (d.Id = app.DomainId)
+                                    WHERE ip.IpAddress = @ip
+                                    AND (app.ApplicationId = @appId OR app.ApplicationId is NULL)
+                                    order by app.ApplicationId desc, ip.IpType asc";
+                cmd.AddParameter("ip", address.ToString());
+                cmd.AddParameter("appId", applicationId);
+
+                return await cmd.FirstOrDefaultAsync();
+            }
+        }
+
+        public async Task> FindWhitelists(int applicationId)
+        {
+            var domains = new List();
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                // ORDER BY appId desc = get specific one first, then the generic one.
+                cmd.CommandText = @"select d.* 
+                                    from WhitelistedDomains d
+                                    left join WhitelistedDomainApps app ON (d.Id = app.DomainId)
+                                    WHERE app.ApplicationId = @appId OR app.ApplicationId is NULL
+                                    order by app.ApplicationId desc";
+                cmd.AddParameter("appId", applicationId);
+
+                var entries = await cmd.ToListAsync();
+                foreach (var entry in entries)
+                {
+                    if (domains.Any(x => x.Id == entry.Id))
+                        continue;
+                    domains.Add(entry);
+                }
+            }
+
+            if (!domains.Any())
+                return domains;
+
+            var ids = string.Join(", ", domains.Select(x => x.Id));
+            var ips = await _unitOfWork.ToListAsync($"SELECT * FROM WhitelistedDomainIps WHERE DomainId IN ({ids})");
+            var ipsPerDomain = ips.GroupBy(x => x.DomainId);
+            foreach (var domainIps in ipsPerDomain)
+            {
+                var domain = domains.First(x => x.Id == domainIps.Key);
+                domain.IpAddresses = domainIps.ToArray();
+            }
+
+            return domains;
+        }
+
+        public async Task SaveIp(WhitelistedDomainIp entry)
+        {
+            await _unitOfWork.InsertAsync(entry);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainApplication.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainApplication.cs
new file mode 100644
index 00000000..8617998f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainApplication.cs
@@ -0,0 +1,10 @@
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    public class WhitelistedDomainApplication
+    {
+        public int ApplicationId { get; set; }
+
+        public int DomainId { get; set; }
+        public int Id { get; set; }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainApplicationMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainApplicationMapper.cs
new file mode 100644
index 00000000..65b27083
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainApplicationMapper.cs
@@ -0,0 +1,11 @@
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    public class WhitelistedDomainApplicationMapper : CrudEntityMapper
+    {
+        public WhitelistedDomainApplicationMapper() : base("WhitelistedDomainApps")
+        {
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainIpMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainIpMapper.cs
new file mode 100644
index 00000000..9650b2ac
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainIpMapper.cs
@@ -0,0 +1,19 @@
+using System.Net;
+using Coderr.Server.App.Modules.Whitelists;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    class WhitelistedDomainIpMapper : CrudEntityMapper
+    {
+        public WhitelistedDomainIpMapper() : base("WhitelistedDomainIps")
+        {
+            Property(x => x.IpAddress)
+                .ToColumnValue(x => x.ToString())
+                .ToPropertyValue(x => IPAddress.Parse((string)x));
+            Property(x => x.IpType)
+                .ToColumnValue(x => (int)x)
+                .ToPropertyValue(x => (IpType)x);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainMapper.cs b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainMapper.cs
new file mode 100644
index 00000000..b6a08fdd
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Modules/Whitelists/WhitelistedDomainMapper.cs
@@ -0,0 +1,17 @@
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Modules.Whitelists
+{
+    class WhitelistMapper : CrudEntityMapper
+    {
+        public WhitelistMapper() : base("WhitelistedDomains")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+            Property(x => x.IpAddresses)
+                .Ignore();
+            Property(x => x.ApplicationIds)
+                .Ignore();
+        }
+    }
+}
diff --git a/src/Server/OneTrueError.SqlServer/ReadMe.md b/src/Server/Coderr.Server.SqlServer/ReadMe.md
similarity index 100%
rename from src/Server/OneTrueError.SqlServer/ReadMe.md
rename to src/Server/Coderr.Server.SqlServer/ReadMe.md
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/AnalyticsRepository.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/AnalyticsRepository.cs
new file mode 100644
index 00000000..2ba962e5
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/AnalyticsRepository.cs
@@ -0,0 +1,286 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.Infrastructure;
+using Coderr.Server.Infrastructure.Security;
+using Coderr.Server.ReportAnalyzer;
+using Coderr.Server.ReportAnalyzer.Inbound.Handlers.Reports;
+using Coderr.Server.ReportAnalyzer.Incidents;
+using Coderr.Server.SqlServer.ReportAnalyzer.Jobs;
+using Coderr.Server.SqlServer.Tools;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer
+{
+    [ContainerService]
+    public class AnalyticsRepository : IAnalyticsRepository
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(AnalyticsRepository));
+        private readonly ReportDtoConverter _reportDtoConverter = new ReportDtoConverter();
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private const int MaxCollectionSize = 2000000;
+
+        public AnalyticsRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork ?? throw new ArgumentNullException(nameof(unitOfWork));
+            _logger.Debug("ReposUOW " + _unitOfWork.GetHashCode());
+        }
+
+        public bool ExistsByClientId(string clientReportId)
+        {
+            if (clientReportId == null) throw new ArgumentNullException("clientReportId");
+
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = "select id from ErrorReports WHERE ErrorId = @Id";
+                cmd.AddParameter("id", clientReportId);
+                return cmd.ExecuteScalar() != null;
+            }
+        }
+
+        public IncidentBeingAnalyzed FindIncidentForReport(int applicationId, string reportHashCode,
+            string hashCodeIdentifier)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"SELECT Incidents.* 
+                        FROM Incidents
+                        WHERE ReportHashCode = @ReportHashCode
+                           AND ApplicationId = @applicationId";
+
+
+                cmd.AddParameter("ReportHashCode", reportHashCode);
+                cmd.AddParameter("applicationId", applicationId);
+
+                var incidents = cmd.ToList();
+                return incidents.FirstOrDefault(incident => incident.HashCodeIdentifier == hashCodeIdentifier);
+            }
+        }
+
+        public void CreateIncident(IncidentBeingAnalyzed incident)
+        {
+            if (incident == null) throw new ArgumentNullException("incident");
+            if (string.IsNullOrEmpty(incident.ReportHashCode))
+                throw new InvalidOperationException("ReportHashCode is required to be able to detect duplicates");
+
+            if (incident.LastReportAtUtc == DateTime.MinValue)
+                incident.LastReportAtUtc = DateTime.UtcNow;
+            if (incident.LastStoredReportUtc == DateTime.MinValue)
+                incident.LastStoredReportUtc = DateTime.UtcNow;
+
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "INSERT INTO Incidents (ReportHashCode, ApplicationId, CreatedAtUtc, HashCodeIdentifier, StackTrace, ReportCount, UpdatedAtUtc, Description, FullName, IsReOpened, LastStoredReportUtc, LastReportAtUtc)" +
+                    " VALUES (@ReportHashCode, @ApplicationId, @CreatedAtUtc, @HashCodeIdentifier, @StackTrace, @ReportCount, @UpdatedAtUtc, @Description, @FullName, 0, @LastStoredReportUtc, @LastReportAtUtc);select SCOPE_IDENTITY();";
+                cmd.AddParameter("Id", incident.Id);
+                cmd.AddParameter("ReportHashCode", incident.ReportHashCode);
+                cmd.AddParameter("ApplicationId", incident.ApplicationId);
+                cmd.AddParameter("CreatedAtUtc", incident.CreatedAtUtc);
+                cmd.AddParameter("HashCodeIdentifier", incident.HashCodeIdentifier);
+                cmd.AddParameter("ReportCount", incident.ReportCount);
+                cmd.AddParameter("UpdatedAtUtc", incident.UpdatedAtUtc);
+                cmd.AddParameter("Description", incident.Description);
+                cmd.AddParameter("StackTrace", incident.StackTrace);
+                cmd.AddParameter("FullName", incident.FullName);
+                cmd.AddParameter("LastStoredReportUtc", incident.LastStoredReportUtc);
+                cmd.AddParameter("LastReportAtUtc", incident.LastReportAtUtc);
+
+                var id = (int) (decimal) cmd.ExecuteScalar();
+                incident.GetType()
+                    .GetProperty("Id", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
+                    .SetValue(incident, id);
+            }
+
+            //_unitOfWork.Insert(incident);
+        }
+
+        public void SaveEnvironmentName(int incidentId, int applicationId, string environmentName)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"declare @environmentId int;
+                                    select @environmentId = id 
+                                    from Environments
+                                    WHERE Name = @name;
+
+                                    if @environmentId is null
+                                    begin
+                                        insert into Environments(Name) VALUES(@name);
+                                        set @environmentId = scope_identity();
+                                        insert into ApplicationEnvironments(EnvironmentId, ApplicationId, DeleteIncidents) VALUES(@environmentId, @applicationId, 0);
+                                        insert into IncidentEnvironments (IncidentId, EnvironmentId) VALUES(@incidentId, @environmentId);
+                                    end
+                                    else
+                                    begin
+                                        INSERT INTO IncidentEnvironments (IncidentId, EnvironmentId)
+                                        SELECT @incidentId, @environmentId
+                                        WHERE NOT EXISTS (SELECT IncidentId, EnvironmentId FROM IncidentEnvironments
+                                                         WHERE IncidentId=@incidentId AND EnvironmentId=@environmentId)
+
+                                        INSERT INTO ApplicationEnvironments (EnvironmentId, ApplicationId, DeleteIncidents)
+                                        SELECT @environmentId, @applicationId, 0
+                                        WHERE NOT EXISTS (SELECT ApplicationId, EnvironmentId FROM ApplicationEnvironments
+                                                         WHERE ApplicationId=@applicationId AND EnvironmentId=@environmentId)
+                                    end";
+                cmd.AddParameter("incidentId", incidentId);
+                cmd.AddParameter("name", environmentName);
+                cmd.AddParameter("applicationId", applicationId);
+                var rows = cmd.ExecuteNonQuery();
+            }
+        }
+
+        public void UpdateIncident(IncidentBeingAnalyzed incident)
+        {
+            if (incident == null) throw new ArgumentNullException("incident");
+            _unitOfWork.Update(incident);
+        }
+
+        public int GetMonthReportCount()
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                var from = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
+                var to = DateTime.UtcNow;
+
+                cmd.CommandText =
+                    "SELECT count(*) FROM IncidentReports WITH (READUNCOMMITTED) WHERE ReceivedAtUtc >= @from ANd ReceivedAtUtc <= @to";
+                cmd.AddParameter("from", from);
+                cmd.AddParameter("to", to);
+                return (int)cmd.ExecuteScalar();
+            }
+        }
+
+        public void AddMissedReport(DateTime date)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"update IgnoredReports set NumberOfReports=NumberOfReports+1 WHERE date = @date;
+                        IF @@ROWCOUNT=0 insert into IgnoredReports(NumberOfReports, Date) values(1, Convert(date, GetUtcDate()));";
+                cmd.AddParameter("date", date.Date);
+                cmd.ExecuteNonQuery();
+            }
+        }
+
+        public async Task StoreReportStats(ReportMapping mapping)
+        {
+            await _unitOfWork.InsertAsync(mapping);
+        }
+
+        public void CreateReport(ErrorReportEntity report)
+        {
+            if (report == null) throw new ArgumentNullException(nameof(report));
+
+            if (string.IsNullOrEmpty(report.Title) && report.Exception != null)
+            {
+                report.Title = report.Exception.Message;
+                if (report.Title == null)
+                    report.Title = "[Exception message was not specified]";
+                else if (report.Title.Length > 100)
+                    report.Title = report.Title.Substring(0, 100);
+            }
+
+            var collections = new List();
+            foreach (var context in report.ContextCollections)
+            {
+                var data = EntitySerializer.Serialize(context);
+                if (data.Length > MaxCollectionSize)
+                {
+                    var tooLargeCtx = new ErrorReportContextCollection(context.Name,
+                        new Dictionary()
+                        {
+                            {
+                                "Error",
+                                $"This collection was larger ({data.Length}bytes) than the threshold of {MaxCollectionSize}bytes"
+                            }
+                        });
+
+                    data = EntitySerializer.Serialize(tooLargeCtx);
+                }
+                collections.Add(data);
+            }
+
+            _unitOfWork.Insert(report);
+
+            var cols = string.Join(", ", collections);
+            var inboound = new InboundCollection
+            {
+                JsonData = $"[{cols}]",
+                ReportId = report.Id
+            };
+            _unitOfWork.Insert(inboound);
+        }
+
+        public string GetAppName(int applicationId)
+        {
+            if (applicationId < 1)
+                throw new ArgumentOutOfRangeException("applicationId", applicationId, "AppId must be a PK");
+
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = "SELECT Name FROM Applications WHERE Id = @id";
+                cmd.AddParameter("id", applicationId);
+                return (string) cmd.ExecuteScalar();
+            }
+        }
+        
+        public IReadOnlyList GetReportsUsingSql()
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = @"SELECT QueueReports.*
+                                    FROM QueueReports
+                                    ORDER BY QueueReports.Id";
+                cmd.Limit(10);
+
+                try
+                {
+                    var reports = new List();
+                    var idsToRemove = new List();
+                    using (var reader = cmd.ExecuteReader())
+                    {
+                        while (reader.Read())
+                        {
+                            var json = "";
+                            try
+                            {
+                                json = (string) reader["body"];
+                                var report = _reportDtoConverter.LoadReportFromJson(json);
+                                var newReport = _reportDtoConverter.ConvertReport(report, (int) reader["ApplicationId"]);
+                                newReport.RemoteAddress = (string) reader["RemoteAddress"];
+
+                                var claims = CoderrDtoSerializer.Deserialize((string) reader["Claims"]);
+                                newReport.User = new ClaimsPrincipal(new ClaimsIdentity(claims, AuthenticationTypes.Default));
+
+                                reports.Add(newReport);
+                                idsToRemove.Add(reader.GetInt32(0));
+                            }
+                            catch (Exception ex)
+                            {
+                                _logger.Error("Failed to deserialize " + json, ex);
+                            }
+                        }
+                    }
+                    if (idsToRemove.Any())
+                        _unitOfWork.ExecuteNonQuery("DELETE FROM QueueReports WHERE Id IN (" +
+                                                    string.Join(",", idsToRemove) + ")");
+                    return reports;
+                }
+                catch (Exception ex)
+                {
+                    throw cmd.CreateDataException(ex);
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Environments/EnvironmentFilter.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Environments/EnvironmentFilter.cs
new file mode 100644
index 00000000..7eef8d72
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Environments/EnvironmentFilter.cs
@@ -0,0 +1,35 @@
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.ReportAnalyzer.Inbound;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Environments
+{
+    /// 
+    ///     Checks if the specified environment is configured to delete incidents.
+    /// 
+    [ContainerService]
+    internal class EnvironmentFilter : IReportFilter
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public EnvironmentFilter(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task Filter(FilterContext context)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"SELECT DeleteIncidents 
+                                    FROM ApplicationEnvironments
+                                    JOIN Environments ON (EnvironmentId=Environments.Id)
+                                    WHERE Name = @name";
+                cmd.AddParameter("name", context.ErrorReport.EnvironmentName);
+                var deleteIncidents = await cmd.ExecuteScalarAsync();
+                return deleteIncidents?.Equals(true) == true ? FilterResult.DiscardReport : FilterResult.FullAnalyzis;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/ErrorReportEntityMapper.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/ErrorReportEntityMapper.cs
new file mode 100644
index 00000000..833e39a5
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/ErrorReportEntityMapper.cs
@@ -0,0 +1,34 @@
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.SqlServer.Tools;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer
+{
+    public class ErrorReportEntityMapper : CrudEntityMapper
+    {
+        public ErrorReportEntityMapper() : base("ErrorReports")
+        {
+            Property(x => x.Id)
+                .PrimaryKey(true);
+
+            Property(x => x.Exception)
+                .ToPropertyValue(EntitySerializer.Deserialize)
+                .ToColumnValue(EntitySerializer.Serialize);
+
+            Property(x => x.ContextCollections)
+                .ColumnName("ContextInfo")
+                .ToPropertyValue(x => null)
+                .ToColumnValue(x => "");
+
+            Property(x => x.EnvironmentName)
+                .Ignore();
+
+            Property(x => x.ClientReportId)
+                .ColumnName("ErrorId");
+
+            Property(x => x.User)
+                .Ignore();
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/ErrorReportRepository.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/ErrorReportRepository.cs
new file mode 100644
index 00000000..a3078077
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/ErrorReportRepository.cs
@@ -0,0 +1,185 @@
+//using System;
+//using System.Collections.Generic;
+//using System.Data.Common;
+//using System.Linq;
+//using System.Threading.Tasks;
+//using Coderr.Server.ReportAnalyzer.Abstractions;
+//using Griffin.Data;
+//using Griffin.Data.Mapper;
+//using Coderr.Core;
+//using Coderr.ReportsAnalytics.Reports;
+//using Coderr.UnitOfWork;
+
+//namespace Coderr.ReportAnalytics.Data.SqlServer
+//{
+
+//    //    public interface IReportsRepository
+//    //    {
+//    //        Task Create(InvalidErrorReport invalidReport);
+//    //        /// 
+//    //        ///     Customer specific id.
+//    //        /// 
+//    //        /// 
+//    //        /// 
+//    //        //Task FindByErrorIdAsync(string errorId);
+//    //        Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize);
+
+//    //        /// 
+//    //        /// Finds the by error identifier asynchronous.
+//    //        /// 
+//    //        /// Customer generated id (from the client library).
+//    //        /// 
+//    //        Task FindByErrorIdAsync(string errorId);
+//    //    }
+
+
+//    [ContainerService]
+//    internal class ErrorReportRepository// : IReportsRepository
+//    {
+//        private IAdoNetUnitOfWork _uow;
+
+//        public ErrorReportRepository(CustomerUnitOfWork uow)
+//        {
+//            if (uow == null) throw new ArgumentNullException("uow");
+
+//            _uow = uow;
+//        }
+
+//        public void Create(ErrorReportEntity entity)
+//        {
+//            using (var cmd = _uow.CreateCommand())
+//            {
+//                cmd.CommandText = "INSERT INTO ErrorReports (Id, ErrorId, ApplicationId, Title, Exception, ReportHashCode, HashCodeIdentifier, IncidentId, CreatedAtUtc, ContextInfo)"
+//                                  +
+//                                  " VALUES (@Id, @ErrorId, @ApplicationId, @Title, @Exception, @ReportHashCode, @HashCodeIdentifier, @IncidentId, @CreatedAtUtc, @ContextInfo)";
+
+//                var ex = JsonSerializer.Serialize(entity.Exception);
+//                var contexts = JsonSerializer.Serialize(entity.ContextInfo);
+//                cmd.AddParameter("Id", entity.Id);
+//                cmd.AddParameter("ErrorId", entity.ClientReportId);
+//                cmd.AddParameter("ApplicationId", entity.ApplicationId);
+//                cmd.AddParameter("Exception", ex);
+//                cmd.AddParameter("ReportHashCode", entity.ReportHashCode);
+//                cmd.AddParameter("HashCodeIdentifier", entity.HashCodeIdentifier);
+//                cmd.AddParameter("IncidentId", entity.IncidentId);
+//                cmd.AddParameter("CreatedAtUtc", entity.CreatedAtUtc);
+//                cmd.AddParameter("ContextInfo", contexts);
+
+//                if (entity.Exception != null)
+//                {
+//                    var pos = entity.Exception.Message.IndexOfAny(new[] { '\r', '\n' });
+//                    cmd.AddParameter("Title", pos == -1
+//                        ? entity.Exception.Message
+//                        : entity.Exception.Message.Substring(0, pos));
+//                }
+//                else
+//                    cmd.AddParameter("Title", "");
+
+//                cmd.ExecuteNonQuery();
+//            }
+//        }
+
+
+//        public void Update(ErrorReportEntity entity)
+//        {
+//            var ex = JsonSerializer.Serialize(entity.Exception);
+//            var contexts = JsonSerializer.Serialize(entity.ContextInfo);
+
+
+//            using (var cmd = _uow.CreateCommand())
+//            {
+//                cmd.CommandText = @"UPDATE ErrorReports SET ErrorId = @ErrorId,
+//ApplicationId = @ApplicationId,
+//Exception = @Exception,
+//ReportHashCode = @ReportHashCode,
+//HashCodeIdentifier = @HashCodeIdentifier,
+//IncidentId = @incidentId,
+//CreatedAtUtc = @CreatedAtUtc,
+//ContextInfo = @ContextInfo
+//WHERE Id = @id";
+
+//                cmd.AddParameter("ErrorId", entity.ClientReportId);
+//                cmd.AddParameter("ApplicationId", entity.ApplicationId);
+//                cmd.AddParameter("Exception", ex);
+//                cmd.AddParameter("ReportHashCode", entity.ReportHashCode);
+//                cmd.AddParameter("HashCodeIdentifier", entity.HashCodeIdentifier);
+//                cmd.AddParameter("IncidentId", entity.IncidentId);
+//                cmd.AddParameter("CreatedAtUtc", entity.CreatedAtUtc);
+//                cmd.AddParameter("ContextInfo", contexts);
+//                cmd.AddParameter("Id", entity.Id);
+//                cmd.ExecuteNonQuery();
+//            }
+//        }
+
+
+//        //public async Task Create(InvalidErrorReport entity)
+//        //{
+//        //    using (var cmd = (DbCommand)_uow.CreateCommand())
+//        //    {
+//        //        cmd.CommandText =
+//        //            @"INSERT INTO InvalidErrorReports (Id, AddedAtUtc, ApplicationId, Body, Exception) VALUES(@Id, @AddedAtUtc, @OrganizationId, @ApplicationId, @Body, @Exception)";
+//        //        cmd.AddParameter("Id", entity.Id);
+//        //        cmd.AddParameter("AddedAtUtc", entity.AddedAtUtc);
+//        //        cmd.AddParameter("ApplicationId", entity.ApplicationId);
+//        //        cmd.AddParameter("Body", entity.Report);
+//        //        cmd.AddParameter("Exception", entity.Exception);
+//        //        await cmd.ExecuteNonQueryAsync();
+//        //    }
+//        //}
+
+
+//        //public async Task FindByErrorIdAsync(string errorId)
+//        //{
+//        //    using (var cmd = (DbCommand)_uow.CreateCommand())
+//        //    {
+//        //        cmd.CommandText =
+//        //            "SELECT * FROM ErrorReports WHERE ErrorId = @id";
+
+//        //        cmd.AddParameter("id", errorId);
+//        //        return await cmd.FirstOrDefaultAsync();
+//        //    }
+//        //}
+
+//        //public async Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize)
+//        //{
+//        //    using (var cmd = (DbCommand)_uow.CreateCommand())
+//        //    {
+//        //        cmd.AddParameter("incidentId", incidentId);
+//        //        long totalRows = 0;
+//        //        if (pageNumber > 0)
+//        //        {
+//        //            cmd.CommandText =
+//        //"SELECT count(*) FROM ErrorReports WHERE IncidentId = @incidentId";
+//        //            totalRows = (int)await cmd.ExecuteScalarAsync();
+//        //        }
+
+//        //        cmd.CommandText =
+//        //            "SELECT * FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY CreatedAtUtc DESC";
+//        //        if (pageNumber > 0)
+//        //        {
+//        //            var offset = (pageNumber - 1)*pageSize;
+//        //            cmd.CommandText += string.Format(@" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize);
+//        //        }
+
+//        //        //cmd.AddParameter("incidentId", incidentId);
+//        //        var list = await cmd.ToListAsync();
+//        //        return new PagedReports()
+//        //        {
+//        //            TotalCount = (int) totalRows,
+//        //            Reports = (IReadOnlyList)list
+//        //        };
+//        //    }
+//        //}
+
+//        public async Task> GetForIncidentAsync(int incidentId)
+//        {
+//            using (var cmd = (DbCommand)_uow.CreateCommand())
+//            {
+//                cmd.CommandText = "SELECT * FROM ErrorReports WHERE IncidentId = @id";
+//                cmd.AddParameter("id", incidentId);
+//                return await cmd.ToListAsync();
+//            }
+//        }
+//    }
+//}
+
diff --git a/src/Server/OneTrueError.SqlServer/Core/Feedback/LookupReportsForFeedback.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Feedback/LookupReportsForFeedback.cs
similarity index 78%
rename from src/Server/OneTrueError.SqlServer/Core/Feedback/LookupReportsForFeedback.cs
rename to src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Feedback/LookupReportsForFeedback.cs
index 85fb81be..ec9c0f18 100644
--- a/src/Server/OneTrueError.SqlServer/Core/Feedback/LookupReportsForFeedback.cs
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Feedback/LookupReportsForFeedback.cs
@@ -1,116 +1,120 @@
-using System.Collections.Generic;
-using System.Data.Common;
-using System.Threading.Tasks;
-using Griffin.ApplicationServices;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using log4net;
-using OneTrueError.App.Core.Feedback;
-
-namespace OneTrueError.SqlServer.Core.Feedback
-{
-    //TODO: invent some way to execute jobs for all customer databases.
-    //[Component(RegisterAsSelf = true)]
-    public class LookupReportsForFeedback : IBackgroundJobAsync
-    {
-        private readonly ILog _logger = LogManager.GetLogger(typeof(LookupReportsForFeedback));
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-
-        public LookupReportsForFeedback(IAdoNetUnitOfWork unitOfWork)
-        {
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task ExecuteAsync()
-        {
-            var items = new List();
-            await GetPendingFeedback(items);
-            await LookupReportInfo(items);
-            foreach (var item in items)
-            {
-                if (item.CanUpdate)
-                {
-                    using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
-                    {
-                        _logger.Debug("Attaching report to " + item.ReportId + " to feedback " + item.Id);
-                        cmd.CommandText =
-                            "UPDATE IncidentFeedback SET IncidentId=@incidentId, ApplicationId = @appId, ReportId = @reportId WHERE Id = @id";
-                        cmd.AddParameter("incidentId", item.IncidentId);
-                        cmd.AddParameter("appId", item.ApplicationId);
-                        cmd.AddParameter("reportId", item.ReportId);
-                        cmd.AddParameter("id", item.Id);
-                        await cmd.ExecuteNonQueryAsync();
-                    }
-                }
-
-                else if (item.CanRemove)
-                {
-                    using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
-                    {
-                        _logger.Debug("Deleting feedback " + item.Id);
-                        cmd.CommandText =
-                            "DELETE FROM IncidentFeedback WHERE Id = @id";
-                        cmd.AddParameter("id", item.Id);
-                        await cmd.ExecuteNonQueryAsync();
-                    }
-                }
-                else
-                {
-                    _logger.Debug("Paria: " + item.Id + "/" + item.ErrorId);
-                }
-            }
-        }
-
-        private async Task GetPendingFeedback(ICollection items)
-        {
-            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
-            {
-                cmd.CommandText =
-                    "SELECT * FROM incidentfeedback WHERE IncidentId is null";
-                var myItems = await cmd.ToListAsync();
-                foreach (var item in myItems)
-                {
-                    items.Add(item);
-                }
-
-                if (items.Count > 0)
-                    if (items.Count > 0)
-                        _logger.Debug("Added " + items.Count + " items.");
-            }
-        }
-
-        private async Task LookupReportInfo(IEnumerable items)
-        {
-            foreach (var item in items)
-            {
-                using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
-                {
-                    cmd.CommandText = "SELECT Id, ApplicationId, IncidentId FROM ErrorReports WHERE ";
-                    if (item.ErrorId != null)
-                    {
-                        cmd.CommandText += "ErrorId = @id";
-                        cmd.AddParameter("id", item.ErrorId);
-                    }
-                    else
-                    {
-                        cmd.CommandText += "Id = @id";
-                        cmd.AddParameter("id", item.ReportId);
-                    }
-
-                    using (var reader = await cmd.ExecuteReaderAsync())
-                    {
-                        if (!await reader.ReadAsync())
-                            continue;
-
-                        item.AssignToReport((int) reader["Id"],
-                            (int) reader["IncidentId"],
-                            (int) reader["ApplicationId"]);
-                        _logger.Debug("Identified report " + item.ReportId + "/" + item.ReportId + " for feedback " +
-                                      item.Id);
-                    }
-                }
-            }
-        }
-    }
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.Domain.Core.Feedback;
+using Griffin.ApplicationServices;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+using log4net;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Feedback
+{
+    [ContainerService(RegisterAsSelf = true)]
+    public class LookupReportsForFeedback : IBackgroundJobAsync
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(LookupReportsForFeedback));
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+
+        public LookupReportsForFeedback(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task ExecuteAsync()
+        {
+            var items = new List();
+            await GetPendingFeedback(items);
+            await LookupReportInfo(items);
+            foreach (var item in items)
+            {
+                if (item.CanUpdate)
+                {
+                    using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+                    {
+                        _logger.Debug("Attaching report to " + item.ReportId + " to feedback " + item.Id);
+                        cmd.CommandText =
+                            "UPDATE IncidentFeedback SET IncidentId=@incidentId, ApplicationId = @appId, ReportId = @reportId WHERE Id = @id";
+                        cmd.AddParameter("incidentId", item.IncidentId);
+                        cmd.AddParameter("appId", item.ApplicationId);
+                        cmd.AddParameter("reportId", item.ReportId);
+                        cmd.AddParameter("id", item.Id);
+                        await cmd.ExecuteNonQueryAsync();
+                    }
+                }
+
+                else if (item.CanRemove)
+                {
+                    using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+                    {
+                        _logger.Debug("Deleting feedback " + item.Id);
+                        cmd.CommandText =
+                            "DELETE FROM IncidentFeedback WHERE Id = @id";
+                        cmd.AddParameter("id", item.Id);
+                        await cmd.ExecuteNonQueryAsync();
+                    }
+                }
+                else
+                {
+                    _logger.Debug("Paria: " + item.Id + "/" + item.ErrorId);
+                }
+            }
+        }
+
+        private async Task GetPendingFeedback(ICollection items)
+        {
+            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "SELECT * FROM incidentfeedback WHERE IncidentId is null";
+                var myItems = await cmd.ToListAsync();
+                foreach (var item in myItems)
+                {
+                    items.Add(item);
+                }
+
+                if (items.Count > 0)
+                    if (items.Count > 0)
+                        _logger.Debug("Added " + items.Count + " items.");
+            }
+        }
+
+        private async Task LookupReportInfo(IEnumerable items)
+        {
+            foreach (var item in items)
+            {
+                using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+                {
+                    if (item.ErrorId != null)
+                    {
+                        cmd.CommandText = @"SELECT IncidentReports.Id, ApplicationId, IncidentId 
+                                                FROM IncidentReports WITH (READUNCOMMITTED)
+                                                JOIN Incidents WITH(READUNCOMMITTED)  ON (Incidents.Id = IncidentId)
+                                                WHERE ErrorId = @id";
+                        cmd.AddParameter("id", item.ErrorId);
+                    }
+                    else
+                    {
+                        cmd.CommandText = @"SELECT Id, ApplicationId, IncidentId 
+                                            FROM ErrorReports 
+                                            WHERE Id = @id";
+                        cmd.AddParameter("id", item.ReportId);
+                    }
+
+                    using (var reader = await cmd.ExecuteReaderAsync())
+                    {
+                        if (!await reader.ReadAsync())
+                            continue;
+
+                        item.AssignToReport((int) reader["Id"],
+                            (int) reader["IncidentId"],
+                            (int) reader["ApplicationId"]);
+                        _logger.Debug("Identified report " + item.ReportId + "/" + item.ReportId + " for feedback " +
+                                      item.Id);
+                    }
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Handlers/ProcessInboundCollectionsHandler.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Handlers/ProcessInboundCollectionsHandler.cs
new file mode 100644
index 00000000..d8f6ec57
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Handlers/ProcessInboundCollectionsHandler.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+using System.Data.SqlClient;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions;
+using Coderr.Server.Domain.Core.ErrorReports;
+using Coderr.Server.ReportAnalyzer;
+using Coderr.Server.ReportAnalyzer.Abstractions.Incidents;
+using Coderr.Server.SqlServer.ReportAnalyzer.Jobs;
+using Coderr.Server.SqlServer.Tools;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Handlers
+{
+    /// 
+    ///     Process collections that was attached to inbound reports
+    /// 
+    /// 
+    ///     
+    ///         Will convert them.
+    ///     
+    /// 
+    internal class ProcessInboundContextCollectionsHandler : IMessageHandler
+    {
+        private static int _isProcessing;
+        private readonly Importer _importer;
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public ProcessInboundContextCollectionsHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+            var db = (IGotTransaction) unitOfWork;
+
+            // Use CoderrDbTransaction
+            SqlTransaction transaction;
+            var innerTransaction = db.Transaction.GetType().GetProperty("Inner")?.GetValue(db.Transaction);
+            if (innerTransaction != null)
+            {
+                transaction = (SqlTransaction)innerTransaction;
+            }
+            else
+            {
+                transaction = (SqlTransaction)db.Transaction;
+            }
+
+            _importer = new Importer(transaction);
+        }
+
+
+        public async Task HandleAsync(IMessageContext context, ProcessInboundContextCollections message)
+        {
+            // We can receive multiple reports simultaneously.
+            // Make sure that we only got one handler running.
+            if (Interlocked.CompareExchange(ref _isProcessing, 1, 0) == 1)
+                return;
+
+            try
+            {
+                var collections = await GetInboundCollections();
+                foreach (var collection in collections)
+                {
+                    var contexts = EntitySerializer.Deserialize(collection.JsonData);
+                    _importer.AddContextCollections(collection.ReportId, contexts);
+                }
+
+                if (collections.Any())
+                {
+                    await _importer.Execute();
+                    await DeleteImportedRows(collections);
+                    _importer.Clear();
+                }
+            }
+            finally
+            {
+                Interlocked.Exchange(ref _isProcessing, 0);
+            }
+        }
+
+
+        private async Task DeleteImportedRows(IEnumerable collections)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                var ids = string.Join(",", collections.Select(x => x.Id).ToArray());
+                var sql = $"DELETE FROM ErrorReportCollectionInbound WHERE Id IN ({ids})";
+                cmd.CommandText = sql;
+                await cmd.ExecuteNonQueryAsync();
+            }
+        }
+
+
+        private async Task> GetInboundCollections()
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText = "SELECT TOP(50) Id, ReportId, Body FROM ErrorReportCollectionInbound";
+                return await cmd.ToListAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs
new file mode 100644
index 00000000..60a507e8
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/IncidentBeingAnalyzedMapper.cs
@@ -0,0 +1,40 @@
+using Coderr.Server.ReportAnalyzer.Incidents;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer
+{
+    public class IncidentBeingAnalyzedMapper : CrudEntityMapper
+    {
+        public IncidentBeingAnalyzedMapper()
+            : base("Incidents")
+        {
+            Property(x => x.UpdatedAtUtc)
+                .ToPropertyValue(DbConverters.ToEntityDate)
+                .ToColumnValue(DbConverters.ToNullableSqlDate);
+
+            Property(x => x.PreviousSolutionAtUtc)
+                .ToPropertyValue(DbConverters.ToEntityDate)
+                .ToColumnValue(DbConverters.ToNullableSqlDate);
+
+            Property(x => x.ReOpenedAtUtc)
+                .ToPropertyValue(DbConverters.ToEntityDate)
+                .ToColumnValue(DbConverters.ToNullableSqlDate);
+
+            Property(x => x.SolvedAtUtc)
+                .ToPropertyValue(DbConverters.ToEntityDate)
+                .ToColumnValue(DbConverters.ToNullableSqlDate);
+
+            Property(x => x.EnvironmentNames).Ignore();
+
+            Property(x => x.IsClosed)
+                .Ignore();
+
+            Property(x => x.IsIgnored)
+                .Ignore();
+
+            Property(x => x.State)
+                .ToPropertyValue(x => (AnalyzedIncidentState) x)
+                .ToColumnValue(x => (int) x);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/ErrorReportContextCollectionProperty.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/ErrorReportContextCollectionProperty.cs
new file mode 100644
index 00000000..7a25f514
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/ErrorReportContextCollectionProperty.cs
@@ -0,0 +1,19 @@
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Jobs
+{
+    public class ErrorReportContextCollectionProperty
+    {
+        public int Id { get; set; }
+
+        public int ReportId { get; set; }
+
+        /// 
+        ///     Context collection name
+        /// 
+        public string CollectionName { get; set; }
+
+        public string PropertyName { get; set; }
+
+        public string Value { get; set; }
+    }
+
+}
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/ErrorReportContextCollectionPropertyMapper.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/ErrorReportContextCollectionPropertyMapper.cs
new file mode 100644
index 00000000..c63e9cfb
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/ErrorReportContextCollectionPropertyMapper.cs
@@ -0,0 +1,12 @@
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Jobs
+{
+    class ErrorReportContextCollectionPropertyMapper : CrudEntityMapper
+    {
+        public ErrorReportContextCollectionPropertyMapper() : base("ErrorReportCollectionProperties")
+        {
+
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/Importer.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/Importer.cs
new file mode 100644
index 00000000..721b8331
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/Importer.cs
@@ -0,0 +1,95 @@
+using System.Collections.Generic;
+using System.Data;
+using System.Data.Common;
+using System.Data.SqlClient;
+using System.Threading.Tasks;
+using Coderr.Server.Domain.Core.ErrorReports;
+using log4net;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Jobs
+{
+    internal class Importer
+    {
+        private readonly SqlTransaction _transaction;
+        private readonly DataTable _dataTable = new DataTable();
+        private readonly ILog _logger = LogManager.GetLogger(typeof(Importer));
+
+
+        public Importer(SqlTransaction transaction)
+        {
+            _transaction = transaction;
+
+            _dataTable.Columns.Add("ReportId", typeof(int));
+            _dataTable.Columns.Add("Name");
+            _dataTable.Columns.Add("PropertyName");
+            _dataTable.Columns.Add("Value");
+        }
+
+        public void AddContextCollections(int reportId, ErrorReportContextCollection[] contexts)
+        {
+            foreach (var context in contexts)
+            {
+                if (context.Properties.Count > 300)
+                {
+                    _logger.Warn($"Report {reportId}, Ignoring collection {context.Name}, since it got {context.Properties.Count} properties");
+                    continue;
+                }
+
+                foreach (var property in context.Properties)
+                {
+                    if (property.Value == null)
+                        continue;
+
+                    var row = CreateDataTableRow(_dataTable, reportId, context, property);
+                    _dataTable.Rows.Add(row);
+                }
+            }
+        }
+
+        public async Task Execute()
+        {
+            //TODO: Remove once all processing is in a separate library.
+            using (var bulkCopy = new SqlBulkCopy(_transaction.Connection, SqlBulkCopyOptions.Default, _transaction))
+            {
+                bulkCopy.DestinationTableName = "ErrorReportCollectionProperties";
+                bulkCopy.ColumnMappings.Add("ReportId", "ReportId");
+                bulkCopy.ColumnMappings.Add("Name", "Name");
+                bulkCopy.ColumnMappings.Add("PropertyName", "PropertyName");
+                bulkCopy.ColumnMappings.Add("Value", "Value");
+                await bulkCopy.WriteToServerAsync(_dataTable);
+            }
+
+            return _dataTable.Rows.Count;
+        }
+
+        private static DataRow CreateDataTableRow(DataTable dataTable, int reportId,
+            ErrorReportContextCollection context,
+            KeyValuePair property)
+        {
+            var propertyName = property.Key;
+            string contextName = context.Name;
+
+            if (contextName.Length > 50)
+            {
+                contextName = contextName.Substring(0, 47) + "...";
+            }
+
+            if (propertyName.Length > 50)
+            {
+                propertyName = propertyName.Substring(0, 47) + "...";
+            }
+
+            var row = dataTable.NewRow();
+            row["ReportId"] = reportId;
+            row["Name"] = contextName;
+            row["PropertyName"] = propertyName;
+            row["Value"] = property.Value;
+            return row;
+        }
+
+        public void Clear()
+        {
+            _dataTable.Clear();
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/InboundCollection.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/InboundCollection.cs
new file mode 100644
index 00000000..0bf6b212
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/InboundCollection.cs
@@ -0,0 +1,9 @@
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Jobs
+{
+    public class InboundCollection
+    {
+        public int Id { get; set; }
+        public int ReportId { get; set; }
+        public string JsonData { get; set; }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/InboundCollectionMapper.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/InboundCollectionMapper.cs
new file mode 100644
index 00000000..ba1a1aaf
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Jobs/InboundCollectionMapper.cs
@@ -0,0 +1,14 @@
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Jobs
+{
+    class InboundCollectionMapper : CrudEntityMapper
+    {
+        public InboundCollectionMapper() : base("ErrorReportCollectionInbound")
+        {
+            Property(x => x.JsonData)
+                .ColumnName("Body");
+
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Spikes/ReportSpikeRepository.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Spikes/ReportSpikeRepository.cs
new file mode 100644
index 00000000..05f6c20d
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Spikes/ReportSpikeRepository.cs
@@ -0,0 +1,90 @@
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Boot;
+using Coderr.Server.ReportAnalyzer.ReportSpikes;
+using Coderr.Server.ReportAnalyzer.ReportSpikes.Entities;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Spikes
+{
+    [ContainerService]
+    internal class ReportSpikeRepository : IReportSpikeRepository
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public ReportSpikeRepository(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task> GetWeeksAggregations()
+        {
+            using (var cmd = (DbCommand)_unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"SELECT ps.Id, a.Id ApplicationId, a.Name ApplicationName, TrackedDate, ReportCount, Notified,
+                                            cast(PERCENTILE_CONT(0.50) WITHIN GROUP (ORDER BY ReportCount) OVER (PARTITION BY ApplicationId) as int) as Percentile50,
+                                            cast(PERCENTILE_CONT(0.85) WITHIN GROUP (ORDER BY ReportCount) OVER (PARTITION BY ApplicationId) as int) as Percentile85
+                                    FROM SpikeAggregation ps
+                                    JOIN Applications a ON (a.Id = ApplicationId)
+                                    WHERE TrackedDate >= @sevenDaysAgo
+                                    ORDER BY ApplicationId, TrackedDate";
+                cmd.AddParameter("sevenDaysAgo", DateTime.Today.AddDays(-7));
+                return (IReadOnlyList)await cmd.ToListAsync();
+                //using (var reader = await cmd.ExecuteReaderAsync())
+                //{
+                //    var aggregations = new List();
+                //    while (await reader.ReadAsync())
+                //    {
+                //        var aggregation = new SpikeAggregation
+                //        {
+                //            Id = reader.GetInt32(0),
+                //            ApplicationId = reader.GetInt32(1),
+                //            ApplicationName = reader.GetString(2),
+                //            TrackedDate = reader.GetDateTime(3),
+                //            ReportCount = reader.GetInt32(4),
+                //            Notified = reader.GetBoolean(5),
+                //            Percentile50 = reader.GetInt32(6),
+                //            Percentile85 = reader.GetInt32(7),
+                //        };
+                //        aggregations.Add(aggregation);
+                //    }
+                //}
+
+                //return (int)numbers.Average();
+            }
+        }
+
+
+        public async Task IncreaseReportCount(int applicationId, DateTime when)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"UPDATE SpikeAggregation
+                                    SET ReportCount = ReportCount + 1 
+                                    WHERE ApplicationId = @appId AND TrackedDate = @date
+                                    IF @@ROWCOUNT = 0
+                                      BEGIN
+                                        INSERT SpikeAggregation (ApplicationId, TrackedDate, ReportCount, Notified)
+                                        SELECT @appId, @date, 1, 0
+                                      END";
+                cmd.AddParameter("appId", applicationId);
+                cmd.AddParameter("date", when);
+                await cmd.ExecuteScalarAsync();
+            }
+        }
+
+        public async Task MarkAsNotified(int spikeId)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = @"UPDATE SpikeAggregation SET Notified = 1 WHERE Id = @id";
+                cmd.AddParameter("id", spikeId);
+                await cmd.ExecuteScalarAsync();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Spikes/SpikeAggregationMapper.cs b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Spikes/SpikeAggregationMapper.cs
new file mode 100644
index 00000000..2c75eef4
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/ReportAnalyzer/Spikes/SpikeAggregationMapper.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Coderr.Server.ReportAnalyzer.ReportSpikes.Entities;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.ReportAnalyzer.Spikes
+{
+    class SpikeAggregationMapper : CrudEntityMapper
+    {
+        public SpikeAggregationMapper() : base("SpikeAggregation")
+        {
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v01.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v01.sql
new file mode 100644
index 00000000..4b55ba08
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v01.sql
@@ -0,0 +1,325 @@
+IF OBJECT_ID(N'dbo.[Settings]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[Settings](
+		[Section] [varchar](50) NOT NULL,
+		[Name] [varchar](50) NOT NULL,
+		[Value] [varchar](512),
+	 ) ON [PRIMARY]
+ END
+
+
+IF OBJECT_ID(N'dbo.[Accounts]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[Accounts](
+		[Id] [int] IDENTITY(1,1) NOT NULL,
+		[UserName] [varchar](50) NOT NULL,
+		[HashedPassword] [varchar](512) NOT NULL,
+		[CreatedAtUtc] [datetime] NOT NULL,
+		[Email] [varchar](255) NOT NULL,
+		[Salt] [varchar](512) NOT NULL,
+		[AccountState] [varchar](20) NOT NULL,
+		[TrackingId] [varchar](40) NULL,
+		[LoginAttempts] [int] NOT NULL,
+		[LastLoginAtUtc] [datetime] NULL,
+		[ActivationKey] [varchar](50) NULL,
+		[PromotionCode] [varchar](50) NULL,
+		[UpdatedAtUtc] [datetime] NULL,
+	 CONSTRAINT [accounts_pkey] PRIMARY KEY CLUSTERED ([Id] ASC)
+	 ) ON [PRIMARY]
+ END
+
+ IF OBJECT_ID(N'dbo.[InvalidReports]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[InvalidReports](
+		[Id] [int] IDENTITY(1,1) NOT NULL,
+		[AppKey] [varchar](36) NOT NULL,
+		[Signature] [varchar](36) NOT NULL,
+		[ReportBody] [ntext] NOT NULL,
+		[ErrorMessage] [varchar](2000) NOT NULL,
+		[CreatedAtUtc] [datetime] NOT NULL,
+	 CONSTRAINT [invalidreports_pkey] PRIMARY KEY CLUSTERED ([Id] ASC)
+	) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
+END
+
+
+IF OBJECT_ID(N'dbo.[Invitations]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[Invitations](
+		[Id] [int] IDENTITY(1,1) NOT NULL,
+		[Email] [varchar](2000) NOT NULL,
+		[InvitationKey] [char](32) NOT NULL,
+		[CreatedAtUtc] [datetime] NOT NULL,
+		[InvitedBy] varchar(50) NOT NULL,
+		[Invitations] varchar(2500) NOT NULL,
+	
+	 CONSTRAINT [invitations_pkey] PRIMARY KEY CLUSTERED  ([Id] ASC)
+	) ON [PRIMARY]
+END
+
+IF OBJECT_ID(N'dbo.[Applications]', N'U') IS NULL
+BEGIN
+		CREATE TABLE [dbo].[Applications] (
+        [Id]              INT           IDENTITY (1, 1) NOT NULL primary key,
+        [Name]            NVARCHAR (50) NOT NULL,
+        [AppKey]          VARCHAR (36)  NOT NULL,
+        [CreatedById]     INT           NOT NULL,
+        [CreatedAtUtc]    DATETIME      NOT NULL,
+        [ApplicationType] VARCHAR (40)  NOT NULL,
+        [SharedSecret]    VARCHAR (36)  NOT NULL
+	);
+END
+
+IF OBJECT_ID(N'dbo.[CollectionMetadata]', N'U') IS NULL
+BEGIN
+		CREATE TABLE [dbo].[CollectionMetadata] (
+        [Id]            INT           IDENTITY (1, 1) NOT NULL primary key,
+        [Name]          NVARCHAR (50) NOT NULL,
+        [ApplicationId] INT           NOT NULL,
+        [Properties]    NTEXT         NOT NULL
+	);
+END
+
+IF OBJECT_ID(N'dbo.[ErrorOrigins]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[ErrorOrigins] (
+        [Id]             INT            IDENTITY (1, 1) NOT NULL primary key,
+        [IpAddress]      VARCHAR (20)   NOT NULL,
+        [CountryCode]    VARCHAR (5)    NULL,
+        [CountryName]    VARCHAR (30)   NULL,
+        [RegionCode]     VARCHAR (5)    NULL,
+        [RegionName]     VARCHAR (30)   NULL,
+        [City]           NVARCHAR (30)  NULL,
+        [ZipCode]        VARCHAR (10)   NULL,
+        [Latitude]       DECIMAL (9, 6) NOT NULL,
+        [Longitude]      DECIMAL (9, 6) NOT NULL,
+        [CreatedAtUtc]   DATETIME       NOT NULL
+	);
+END
+
+IF OBJECT_ID(N'dbo.[ErrorReportOrigins]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[ErrorReportOrigins] (
+        [ErrorOriginId]             INT NOT NULL,
+        [IncidentId]             INT NOT NULL,
+        [ReportId]    INT  NOT NULL,
+        [ApplicationId]    INT  NOT NULL,
+        [CreatedAtUtc]   DATETIME       NOT NULL
+	);
+END
+
+
+IF OBJECT_ID(N'dbo.[ErrorReports]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[ErrorReports] (
+        [Id]                 INT            IDENTITY (1, 1) NOT NULL primary key,
+        [IncidentId]         INT            NOT NULL,
+        [ErrorId]            VARCHAR (36)   NOT NULL,
+        [ApplicationId]      INT            NOT NULL,
+        [ReportHashCode]     VARCHAR (20)   NOT NULL,
+        [CreatedAtUtc]       DATETIME       NOT NULL,
+        [SolvedAtUtc]        DATETIME       NULL,
+        [Title]						     NVARCHAR(100)  NULL,
+        [RemoteAddress]      VARCHAR (45)   NULL, --SEE http://stackoverflow.com/questions/166132/maximum-length-of-the-textual-representation-of-an-ipv6-address
+        [Exception]          NTEXT          NOT NULL,
+        [ContextInfo]        NTEXT          NOT NULL
+	);
+END
+
+
+IF OBJECT_ID(N'dbo.[ErrorReports_IncidentId]', N'I') IS NULL
+BEGIN
+	CREATE NONCLUSTERED INDEX [ErrorReports_IncidentId]
+        ON [dbo].[ErrorReports]([IncidentId] ASC, [CreatedAtUtc] DESC);
+END
+
+IF OBJECT_ID(N'dbo.[Application_GetWeeklyStats]', N'I') IS NULL
+BEGIN
+	CREATE NONCLUSTERED INDEX [Application_GetWeeklyStats]
+        ON [dbo].[ErrorReports]([ApplicationId] ASC, [CreatedAtUtc] DESC);
+END
+
+IF OBJECT_ID(N'dbo.[IncidentFeedback]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[IncidentFeedback] (
+        [Id]                 INT            IDENTITY (1, 1) NOT NULL primary key,
+        [ApplicationId]      INT            NULL,
+        [IncidentId]         INT            NULL,
+        [ReportId]           INT            NULL,
+        [CreatedAtUtc]       DATETIME       NOT NULL,
+        [RemoteAddress]      VARCHAR (20)   NOT NULL,
+        [Description]        NTEXT          NOT NULL,
+        [EmailAddress]       NVARCHAR (512) NULL,
+        [Conversation]       NTEXT          NOT NULL,
+        [ConversationLength] INT            NOT NULL,
+        [ErrorReportId]      VARCHAR (40)   NOT NULL,
+        [Replied]            INT NOT NULL default 0
+	);
+END
+
+
+IF OBJECT_ID(N'dbo.[Incidents]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[Incidents] (
+		[Id]                      INT            IDENTITY (1, 1) NOT NULL,
+		[ReportHashCode]          VARCHAR (20)   NOT NULL,
+		[ApplicationId]           INT            NOT NULL,
+		[CreatedAtUtc]            DATETIME       NOT NULL,
+		[HashCodeIdentifier]      VARCHAR (1024) NOT NULL,
+		[ReportCount]             INT            NOT NULL,
+		[UpdatedAtUtc]            DATETIME       NULL,
+		[Description]             NTEXT          NOT NULL,
+		[FullName]                NVARCHAR (255) NOT NULL,
+		[Solution]                NTEXT          NULL,
+		[IsSolved]                BINARY (1)     NOT NULL default(0),
+		[IsSolutionShared]        BINARY (1)     NOT NULL default(0),
+		[SolvedAtUtc]             DATETIME       NULL,
+		[StackTrace]              NTEXT          NULL,
+		[IsReOpened]              BIT            NOT NULL default(0),
+		[ReOpenedAtUtc]           DATETIME       NULL,
+		[PreviousSolutionAtUtc]   DATETIME       NULL,
+		[IgnoreReports]           BIT            NOT NULL default(0),
+		[IgnoringReportsSinceUtc] DATETIME       NULL,
+		[IgnoringRequestedBy]     NVARCHAR (50)  NULL,
+		[LastSolutionAtUtc]       DATETIME       NULL
+	);
+END
+
+IF OBJECT_ID(N'dbo.[IncidentTags]', N'U') IS NULL
+BEGIN
+    CREATE TABLE [dbo].[IncidentTags] (
+        [id]          INT          IDENTITY (1, 1) NOT NULL primary key,
+        [IncidentId]  INT          NOT NULL,
+        [TagName]     VARCHAR (40) NOT NULL,
+        [OrderNumber] INT          NOT NULL
+	);
+END
+
+
+IF OBJECT_ID(N'dbo.[IncidentTags_FromIncident]', N'U') IS NULL
+BEGIN
+	CREATE NONCLUSTERED INDEX [IncidentTags_FromIncident]
+        ON [dbo].[IncidentTags]([IncidentId] ASC, [OrderNumber] ASC);
+END
+
+IF OBJECT_ID(N'dbo.[ReportContextInfo]', N'U') IS NULL
+BEGIN
+		CREATE TABLE [dbo].[ReportContextInfo] (
+        [Id]           INT             IDENTITY (1, 1) NOT NULL primary key,
+        [IncidentId]   INT             NOT NULL,
+        [ReportId]     INT             NOT NULL,
+        [CreatedAtUtc] DATETIME        NOT NULL,
+        [UpdatedAtUtc] DATETIME        NULL,
+        [Name]         NVARCHAR (1024) NULL,
+        [Value]        NVARCHAR (20)   NULL,
+        [LargeValue]   NTEXT           NOT NULL
+	);
+END
+
+IF OBJECT_ID(N'dbo.[IncidentContextCollections]', N'U') IS NULL
+BEGIN
+		CREATE TABLE [dbo].[IncidentContextCollections] (
+        [Id]                      INT          IDENTITY (1, 1) NOT NULL primary key,
+        [IncidentId]              INT          NOT NULL,
+        [Name]                    VARCHAR (250) NOT NULL,
+        [Properties]              text NOT NULL
+	);
+END
+
+IF OBJECT_ID(N'dbo.[IncidentContextCollections_IncidentId]', N'U') IS NULL
+BEGIN
+	CREATE NONCLUSTERED INDEX [IncidentContextCollections_IncidentId]
+        ON [dbo].[IncidentContextCollections]([IncidentId] ASC);
+END
+
+
+IF OBJECT_ID(N'dbo.[Triggers]', N'U') IS NULL
+BEGIN
+		CREATE TABLE [dbo].[Triggers] (
+        [Id]                      INT            IDENTITY (1, 1) NOT NULL primary key,
+        [Name]                    NVARCHAR (50)  NOT NULL,
+        [Description]             NVARCHAR (512) NOT NULL,
+        [ApplicationId]           INT            NOT NULL,
+        [Rules]                   NTEXT          NOT NULL,
+        [Actions]                 NTEXT          NOT NULL,
+        [LastTriggerAction]       NVARCHAR (50)  NOT NULL,
+        [RunForNewIncidents]      BIT            NOT NULL,
+        [RunForExistingIncidents] BIT            NOT NULL,
+        [RunForReOpenedIncidents] BIT            NOT NULL
+	);
+END
+
+
+IF OBJECT_ID(N'dbo.[UserNotificationSettings]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[UserNotificationSettings] (
+		[AccountId]        INT          NOT NULL,
+		[ApplicationId]    INT          NOT NULL,
+		[NewIncident]      VARCHAR (20) NOT NULL default 'Disabled',
+		[NewReport]        VARCHAR (20) NOT NULL default 'Disabled',
+		[ReOpenedIncident] VARCHAR (20) NOT NULL default 'Disabled',
+		[WeeklySummary]    VARCHAR (20) NOT NULL default 'Disabled',
+		[ApplicationSpike] VARCHAR (20) NOT NULL default 'Disabled',
+		[UserFeedback]     VARCHAR (20) NOT NULL default 'Disabled'
+	);
+END
+ALTER TABLE [UserNotificationSettings]
+        ADD CONSTRAINT pk_UserNotificationSettings PRIMARY KEY (AccountId, ApplicationId);
+
+CREATE TABLE dbo.Users
+(
+        AccountId				INT NOT NULL primary key,
+        EmailAddress		varchar(255) not null,
+        FirstName				varchar(100),
+        LastName				varchar(100),
+        UserName				varchar(100),
+		MobileNumber		varchar(100)
+	);
+
+
+IF OBJECT_ID(N'dbo.[ApplicationMembers]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[ApplicationMembers] (
+        [AccountId]     INT           NULL foreign key references Accounts (Id),
+        [ApplicationId] INT           NOT NULL foreign key references Applications (Id),
+		[EmailAddress]  nvarchar(255) not null,
+        [AddedAtUtc]    DATETIME      NOT NULL,
+        [AddedByName]   VARCHAR (50)  NOT NULL,
+        [Roles]         VARCHAR (255) NOT NULL
+	);
+END
+
+IF OBJECT_ID(N'dbo.[QueueEvents]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[QueueEvents] (
+        [Id]     INT identity      not    NULL primary key,
+		[ApplicationId] INT NOT NULL, 
+        [CreatedAtUtc]    DATETIME      NOT NULL,
+        [AssemblyQualifiedTypeName]   VARCHAR (255)  NOT NULL,
+        [Body]         text NOT NULL,
+	);
+END
+
+IF OBJECT_ID(N'dbo.[QueueReports]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[QueueReports] (
+        [Id]     INT identity      not    NULL primary key,
+		[ApplicationId] INT NOT NULL, 
+        [CreatedAtUtc]    DATETIME      NOT NULL,
+        [AssemblyQualifiedTypeName]   VARCHAR (255)  NOT NULL,
+        [Body]         text NOT NULL,
+		
+	);
+END
+
+
+IF OBJECT_ID(N'dbo.[QueueFeedback]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[QueueFeedback] (
+        [Id]     INT identity      not    NULL primary key,
+		[ApplicationId] INT NOT NULL, 
+        [CreatedAtUtc]    DATETIME      NOT NULL,
+        [AssemblyQualifiedTypeName]   VARCHAR (255)  NOT NULL,
+        [Body]         text NOT NULL,
+		
+	);
+END
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v02.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v02.sql
new file mode 100644
index 00000000..964b7434
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v02.sql
@@ -0,0 +1,18 @@
+IF OBJECT_ID(N'dbo.[ApiKeys]', N'U') IS NULL
+BEGIN
+	CREATE TABLE [dbo].[ApiKeys] (
+        [Id]     INT identity      not    NULL primary key,
+		[ApplicationName] varchar(40) NOT NULL, 
+        [CreatedAtUtc]    DATETIME      NOT NULL,
+        [CreatedById]   int NOT NULL,
+        [GeneratedKey]   varchar(36) NOT NULL,
+        [SharedSecret]   varchar(36) NOT NULL
+	);
+	CREATE TABLE [dbo].[ApiKeyApplications] (
+        [ApiKeyId]     INT not    NULL,
+		[ApplicationId] INT NOT NULL,
+		Primary key (ApiKeyId, ApplicationId),
+		FOREIGN KEY (ApiKeyId) REFERENCES ApiKeys(Id) ON DELETE CASCADE,
+		FOREIGN KEY (ApplicationId) REFERENCES Applications(Id) ON DELETE NO ACTION
+	);
+END
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v03.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v03.sql
new file mode 100644
index 00000000..cde927e6
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v03.sql
@@ -0,0 +1,41 @@
+ALTER TABLE Incidents ADD PRIMARY KEY (Id);
+ALTER TABLE ErrorReportOrigins WITH CHECK ADD CONSTRAINT FK_ErrorReportOrigins_Reports FOREIGN KEY (ReportId) REFERENCES ErrorReports (Id) ON DELETE CASCADE;
+ALTER TABLE ErrorReports WITH CHECK ADD CONSTRAINT FK_ErrorReports_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE;
+ALTER TABLE CollectionMetadata WITH CHECK ADD CONSTRAINT FK_COLME_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE;
+ALTER TABLE IncidentFeedback WITH CHECK ADD CONSTRAINT FK_IncidentFeedback_incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE;
+ALTER TABLE Incidents WITH CHECK ADD CONSTRAINT FK_Incidents_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE;
+ALTER TABLE IncidentTags WITH CHECK ADD CONSTRAINT FK_IncidentTags_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE;
+ALTER TABLE ReportContextInfo WITH CHECK ADD CONSTRAINT FK_ReportContextInfo_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE;
+ALTER TABLE IncidentContextCollections WITH CHECK ADD CONSTRAINT FK_ICC_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE;
+ALTER TABLE [UserNotificationSettings] WITH CHECK ADD CONSTRAINT FK_UNS_accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE;
+ALTER TABLE [Triggers] WITH CHECK ADD CONSTRAINT FK_Triggers_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE;
+
+DECLARE @ConstraintName nvarchar(200)
+SELECT @ConstraintName = KCU.CONSTRAINT_NAME
+FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC 
+INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
+	ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
+	AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
+	AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
+WHERE
+	KCU.TABLE_NAME = 'ApplicationMembers' AND
+	KCU.COLUMN_NAME = 'AccountId'
+IF @ConstraintName IS NOT NULL
+BEGIN
+	EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName)
+END;
+SELECT @ConstraintName = KCU.CONSTRAINT_NAME
+FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC 
+INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
+	ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
+	AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
+	AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
+WHERE
+	KCU.TABLE_NAME = 'ApplicationMembers' AND
+	KCU.COLUMN_NAME = 'ApplicationId'
+IF @ConstraintName IS NOT NULL
+BEGIN
+	EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName)
+END
+ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE;
+ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v04.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v04.sql
new file mode 100644
index 00000000..be6918ec
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v04.sql
@@ -0,0 +1,5 @@
+--version 1.0 (part A) of OneTrueError
+
+ALTER TABLE Accounts ADD IsSysAdmin bit not null default 0;
+alter table ApplicationMembers add Id int identity not null primary key;
+ALTER TABLE ApplicationMembers ALTER COLUMN [EmailAddress]  nvarchar(255) null;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v05.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v05.sql
new file mode 100644
index 00000000..36d1a4c5
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v05.sql
@@ -0,0 +1,3 @@
+--version 1.0 of OneTrueError
+--a split was required due to updating created column
+UPDATE Accounts SET IsSysAdmin = 1 WHERE Id = (SELECT TOP 1 Id FROM ACCOUNTS ORDER BY Id);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v06.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v06.sql
new file mode 100644
index 00000000..f65224d4
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v06.sql
@@ -0,0 +1,39 @@
+create table dbo.ApplicationVersions
+(
+	Id int not null identity primary key,
+	ApplicationId int not null foreign key references Applications (Id),
+	ApplicationName varchar(40) not null,
+	FirstReportDate datetime not null,
+	LastReportDate datetime not null,
+	Version varchar(10) not null
+);
+
+create table dbo.ApplicationVersionMonths
+(
+	Id int not null identity primary key,
+	VersionId int not null foreign key references ApplicationVersions (Id),
+	YearMonth date not null,
+	IncidentCount int not null,
+	ReportCount int not null,
+	LastUpdateAtUtc datetime not null
+);
+
+create table dbo.IncidentVersions
+(
+	IncidentId int not null constraint FK_IncidentVersions_Incidents references Incidents(Id),
+	VersionId int not null constraint FK_IncidentVersions_ApplicationVersions references ApplicationVersions(Id)
+);
+
+IF COL_LENGTH('dbo.Incidents', 'StackTrace') IS NULL
+BEGIN
+ALTER TABLE Incidents ADD StackTrace varchar(MAX);
+
+UPDATE Incidents
+	SET StackTrace = (
+	SELECT TOP (1) Substring(Exception, 
+					CHARINDEX('"StackTrace":"', Exception) + 14, 
+					DATALENGTH(exception)-CHARINDEX('"StackTrace":"', Exception) - 14 - 1)
+	FROM ErrorReports
+	WHERE IncidentId = Incidents.Id)
+
+END
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v07.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v07.sql
new file mode 100644
index 00000000..a8bfb54c
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v07.sql
@@ -0,0 +1,12 @@
+CREATE TABLE dbo.MessageQueue 
+( 
+    Id int not null identity primary key,
+    QueueName varchar(40) not null,
+    CreatedAtUtc datetime not null,
+    MessageType varchar(512) not null,
+    Body nvarchar(MAX) not null
+);
+
+DROP TABLE QueueEvents;
+DROP TABLE QueueFeedback;
+DROP TABLE QueueReports;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v08.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v08.sql
new file mode 100644
index 00000000..cfcf1a52
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v08.sql
@@ -0,0 +1,34 @@
+ALTER TABLE Incidents ADD State int not null default(0);
+ALTER TABLE Incidents ADD AssignedToId int;
+ALTER TABLE Incidents ADD AssignedAtUtc datetime;
+ALTER TABLE Incidents ADD LastReportAtUtc datetime;
+
+DECLARE @ConstraintName nvarchar(200)
+SELECT @ConstraintName = d.Name 
+	FROM SYS.DEFAULT_CONSTRAINTS d, sys.columns c
+	WHERE c.name = 'IsSolved'
+	AND c.object_id = OBJECT_ID(N'Incidents')
+	AND d.PARENT_COLUMN_ID = c.column_id
+EXEC('ALTER TABLE Incidents DROP CONSTRAINT ' + @ConstraintName)
+alter table Incidents drop column IsSolved;
+
+SELECT @ConstraintName = d.Name 
+	FROM SYS.DEFAULT_CONSTRAINTS d, sys.columns c
+	WHERE c.name = 'IgnoreReports'
+	AND c.object_id = OBJECT_ID(N'Incidents')
+	AND d.PARENT_COLUMN_ID = c.column_id
+EXEC('ALTER TABLE Incidents DROP CONSTRAINT ' + @ConstraintName)
+alter table Incidents drop column IgnoreReports;
+
+-- ApiKey module deletes relations manually.
+SELECT 
+    @ConstraintName = KCU.CONSTRAINT_NAME
+FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC 
+INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
+    ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
+    AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
+    AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
+WHERE
+    KCU.TABLE_NAME = 'ApiKeyApplications' AND
+    KCU.COLUMN_NAME = 'ApplicationId'
+IF @ConstraintName IS NOT NULL EXEC('alter table ApiKeyApplications drop  CONSTRAINT ' + @ConstraintName)
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v09.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v09.sql
new file mode 100644
index 00000000..6086424f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v09.sql
@@ -0,0 +1,64 @@
+DECLARE @ConstraintName nvarchar(200)
+SELECT @ConstraintName = KCU.CONSTRAINT_NAME
+FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC 
+INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
+	ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
+	AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
+	AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
+WHERE
+	KCU.TABLE_NAME = 'ApplicationVersionMonths' AND
+	KCU.COLUMN_NAME = 'VersionId'
+IF @ConstraintName IS NOT NULL
+BEGIN
+	EXEC('ALTER TABLE ApplicationVersionMonths DROP CONSTRAINT ' + @ConstraintName)
+END;
+ALTER TABLE ApplicationVersionMonths WITH CHECK ADD CONSTRAINT FK_ApplicationVersionMonths_ApplicationVersions FOREIGN KEY (VersionId) REFERENCES ApplicationVersions (Id) ON DELETE CASCADE;
+
+
+
+SELECT @ConstraintName = KCU.CONSTRAINT_NAME
+FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC 
+INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
+	ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
+	AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
+	AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
+WHERE
+	KCU.TABLE_NAME = 'ApplicationVersions' AND
+	KCU.COLUMN_NAME = 'ApplicationId'
+IF @ConstraintName IS NOT NULL
+BEGIN
+	EXEC('ALTER TABLE ApplicationVersions DROP CONSTRAINT ' + @ConstraintName)
+END;
+ALTER TABLE ApplicationVersions WITH CHECK ADD CONSTRAINT FK_ApplicationVersions_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE;
+
+SELECT @ConstraintName = KCU.CONSTRAINT_NAME
+FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC 
+INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU
+	ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG  
+	AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA 
+	AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME
+WHERE
+	KCU.TABLE_NAME = 'IncidentVersions' AND
+	KCU.COLUMN_NAME = 'IncidentId'
+IF @ConstraintName IS NOT NULL
+BEGIN
+	EXEC('ALTER TABLE IncidentVersions DROP CONSTRAINT ' + @ConstraintName)
+END;
+ALTER TABLE IncidentVersions WITH CHECK ADD CONSTRAINT FK_IncVersions_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE;
+
+create table dbo.ErrorReportCollectionProperties
+(
+	Id int identity not null primary key,
+	ReportId int not null constraint FK_ErrorReportCollectionProperties_ErrorReports REFERENCES ErrorReports(Id) ON DELETE CASCADE,
+	Name varchar(50) not null,
+	PropertyName varchar(50) not null,
+	Value nvarchar(MAX)  not null
+);
+
+
+create table dbo.ErrorReportCollectionInbound
+(
+	Id int identity not null primary key,
+	ReportId int not null constraint FK_ErrorReportCollectionInbound_ErrorReports REFERENCES ErrorReports(Id) ON DELETE CASCADE,
+	Body nvarchar(max) not null
+);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v10.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v10.sql
new file mode 100644
index 00000000..ff91e8b5
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v10.sql
@@ -0,0 +1,19 @@
+create table dbo.IncidentEnvironments
+(
+    Id int not null identity primary key,
+    IncidentId int not null constraint FK_IncidentEnvironment_Incidents REFERENCES Incidents(Id) ON DELETE CASCADE,
+    EnvironmentName varchar(50) not null
+);
+
+create table dbo.IncidentHistory
+(
+    Id int not null identity primary key,
+    IncidentId int not null constraint FK_IncidentHistory_Incidents REFERENCES Incidents(Id) ON DELETE CASCADE,
+    CreatedAtUtc datetime not null,
+    AccountId int NULL, -- for system entries
+    State int not null,
+    ApplicationVersion varchar(40) NULL -- for action where version is not related to the action
+);
+go
+alter table Incidents add IgnoredUntilVersion varchar(20) null;
+CREATE NONCLUSTERED INDEX IX_IncidentHistory_Incidents ON dbo.IncidentHistory (IncidentId);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v11.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v11.sql
new file mode 100644
index 00000000..72a7800d
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v11.sql
@@ -0,0 +1 @@
+--Purpose of this script is just to get in phase with Live and OnPrem
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v12.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v12.sql
new file mode 100644
index 00000000..72a7800d
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v12.sql
@@ -0,0 +1 @@
+--Purpose of this script is just to get in phase with Live and OnPrem
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v13.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v13.sql
new file mode 100644
index 00000000..86461fac
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v13.sql
@@ -0,0 +1,8 @@
+create table dbo.ErrorReportSpikes
+(
+    Id int identity not null primary key,
+    ApplicationId int not null constraint FK_ErrorReportSpikes_Applications REFERENCES Applications(Id) ON DELETE CASCADE,
+    SpikeDate datetime not null,
+    Count int not null,
+    NotifiedAccounts varchar(max) not null
+);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v14.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v14.sql
new file mode 100644
index 00000000..39e970e9
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v14.sql
@@ -0,0 +1 @@
+alter table ApplicationVersions alter column Version varchar(20) not null;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v15.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v15.sql
new file mode 100644
index 00000000..0cc709c6
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v15.sql
@@ -0,0 +1,10 @@
+create Table dbo.IgnoredReports
+(
+    Id int not null identity primary key,
+    NumberOfReports int not null,
+    Date datetime not null
+);
+
+alter table Applications add NumberOfFtes decimal;
+alter table Applications add [EstimatedNumberOfErrors] int;
+alter table Applications add [MuteStatisticsQuestion] bit not null default 0;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v16.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v16.sql
new file mode 100644
index 00000000..276ee38b
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v16.sql
@@ -0,0 +1 @@
+alter table applications alter column NumberOfFtes decimal(4,1) null;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v17.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v17.sql
new file mode 100644
index 00000000..33d83088
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v17.sql
@@ -0,0 +1,12 @@
+create table dbo.Environments
+(
+    Id int identity not null primary key,
+    Name varchar(100) not null,
+);
+
+drop table dbo.IncidentEnvironments;
+CREATE TABLE dbo.IncidentEnvironments
+(
+    IncidentId int not null constraint FK_IncidentEnvironment_Incident foreign key references Incidents(Id) ON DELETE CASCADE,
+    EnvironmentId int not null constraint FK_IncidentEnvironment_Environments foreign key  references Environments(Id),
+);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v18.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v18.sql
new file mode 100644
index 00000000..0b472bcb
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v18.sql
@@ -0,0 +1,11 @@
+IF NOT EXISTS (SELECT *  FROM sys.indexes  WHERE name='IDX_ErrorReportCollectionProperties_ReportId' 
+AND object_id = OBJECT_ID('ErrorReportCollectionProperties'))
+begin
+	CREATE INDEX IDX_ErrorReportCollectionProperties_ReportId
+	ON ErrorReportCollectionProperties (ReportId);
+
+	CREATE INDEX IDX_ErrorReportCollectionInbound_ReportId
+	ON ErrorReportCollectionInbound (ReportId);
+
+end
+
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v19.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v19.sql
new file mode 100644
index 00000000..9bc12c23
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v19.sql
@@ -0,0 +1,12 @@
+CREATE TABLE dbo.IncidentReports
+(
+    Id int not null identity primary key,
+    IncidentId int not null constraint FK_IncidentReports_Incidents foreign key references Incidents(Id) on delete cascade,
+    ReceivedAtUtc datetime not null,
+    ErrorId varchar(40) not null
+);
+create index IDX_IncidentReports_IncidentId ON IncidentReports (IncidentId, ReceivedAtUtc);
+
+insert into IncidentReports (IncidentId, ReceivedAtUtc, ErrorId)
+SELECT IncidentId, CreatedAtUtc, ErrorId
+FROM ErrorReports;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v20.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v20.sql
new file mode 100644
index 00000000..8ed84ee2
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v20.sql
@@ -0,0 +1,14 @@
+create table dbo.CorrelationIds
+(
+    Id int identity not null primary key,
+    Value varchar(40) not null
+);
+
+create table dbo.IncidentCorrelations
+(
+    Id int identity not null primary key,
+    CorrelationId int not null constraint FK_IncidentCorrelations_CorrelationIds foreign key references CorrelationIds(Id),
+    IncidentId int not null constraint FK_IncidentCorrelations_Incidents foreign key references Incidents(Id) ON DELETE CASCADE
+);
+
+create unique index IDX_IncidentCorrelations_Pair ON IncidentCorrelations(CorrelationId, IncidentId);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v21.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v21.sql
new file mode 100644
index 00000000..d6dbc45e
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v21.sql
@@ -0,0 +1,21 @@
+create table dbo.WhitelistedDomains
+(
+	Id int not null identity primary key,
+	DomainName varchar(255) not null
+);
+
+create table dbo.WhitelistedDomainApps
+(
+	Id int not null identity primary key,
+	DomainId int not null constraint FK_WhitelistedDomainApps_WhitelistedDomains foreign key references WhitelistedDomains(Id) ON DELETE CASCADE,
+	ApplicationId int not null constraint FK_WhitelistedDomainApps_Applications foreign key references Applications(Id) ON DELETE CASCADE,
+);
+
+create table dbo.WhitelistedDomainIps
+(
+	Id int not null identity primary key,
+	DomainId int not null constraint FK_WhitelistedDomainIps_WhitelistedDomains foreign key references WhitelistedDomains(Id) ON DELETE CASCADE,
+	IpAddress varchar(36) not null,
+	IpType int not null,
+    StoredAtUtc datetime not null
+);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v22.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v22.sql
new file mode 100644
index 00000000..3dac5fed
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v22.sql
@@ -0,0 +1,12 @@
+
+create table dbo.NotificationsBrowser
+(
+    Id int not null identity primary key,
+    AccountId int not null constraint FK_NotificationBrowser_AccountId foreign key references Accounts(Id),
+    Endpoint varchar(255) not null,
+    PublicKey varchar(150) not null,
+    AuthenticationSecret varchar(150) not null,
+    CreatedAtUtc datetime not null,
+    ExpiresAtUtc datetime null
+);
+
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v23.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v23.sql
new file mode 100644
index 00000000..a96356e2
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v23.sql
@@ -0,0 +1,3 @@
+ALTER TABLE Incidents ADD LastStoredReportUtc datetime;
+go
+UPDATE Incidents SET LastStoredReportUtc = LastReportAtUtc;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v24.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v24.sql
new file mode 100644
index 00000000..a624b976
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v24.sql
@@ -0,0 +1 @@
+ALTER TABLE NotificationsBrowser alter column Endpoint varchar(1024) not null;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v25.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v25.sql
new file mode 100644
index 00000000..edce1879
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v25.sql
@@ -0,0 +1,3 @@
+-- remove null to support 
+ALTER TABLE ErrorOrigins ALTER COLUMN IpAddress VARCHAR(20);
+
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v26.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v26.sql
new file mode 100644
index 00000000..08a1161b
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v26.sql
@@ -0,0 +1,7 @@
+ALTER TABLE IncidentContextCollections DROP CONSTRAINT FK_ICC_incidentId;
+ALTER TABLE IncidentCorrelations DROP CONSTRAINT FK_IncidentCorrelations_Incidents;
+ALTER TABLE IncidentEnvironments DROP CONSTRAINT FK_IncidentEnvironment_Incident;
+ALTER TABLE IncidentFeedback DROP CONSTRAINT FK_IncidentFeedback_incidents;
+ALTER TABLE IncidentHistory DROP CONSTRAINT FK_IncidentHistory_Incidents;
+ALTER TABLE IncidentTags DROP CONSTRAINT FK_IncidentTags_incidentId;
+ALTER TABLE IncidentVersions DROP CONSTRAINT FK_IncVersions_Incidents;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v27.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v27.sql
new file mode 100644
index 00000000..62198d2a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v27.sql
@@ -0,0 +1,3 @@
+alter table ErrorOrigins add IsLookedUp bit not null default(0);
+go
+update ErrorOrigins SET IsLookedUp = 1 WHERE CountryCode IS NOT NULL;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v28.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v28.sql
new file mode 100644
index 00000000..4f1797a9
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v28.sql
@@ -0,0 +1,14 @@
+create table SpikeAggregation
+(
+    Id int not null identity primary key,
+    ApplicationId int not null constraint FK_SpikeAggregation_Applications foreign key references Applications(Id) on delete cascade,
+    TrackedDate Date not null,
+    ReportCount int not null,
+    Notified bit not null default 0
+);
+
+insert into SpikeAggregation (ApplicationId, TrackedDate, ReportCount, Notified)
+select i.ApplicationID, convert(date, ir.ReceivedAtUtc), count(*), 0
+from IncidentReports ir
+join incidents i on (incidentid =i.id)
+group by i.ApplicationId, convert(date, ir.ReceivedAtUtc)
diff --git a/src/Server/OneTrueError.Web/Content/ote_bootstrap_variables.css b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v29.sql
similarity index 100%
rename from src/Server/OneTrueError.Web/Content/ote_bootstrap_variables.css
rename to src/Server/Coderr.Server.SqlServer/Schema/Coderr.v29.sql
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v30.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v30.sql
new file mode 100644
index 00000000..feb8a48a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v30.sql
@@ -0,0 +1,9 @@
+alter table Applications add RetentionDays int not null default 60;
+alter table Environments add StoreReports bit not null default 1;
+create table ApplicationEnvironments
+(
+    Id int not null identity primary key,
+    ApplicationId int not null constraint FK_ApplicationEnvironments_Applications foreign key references Applications(Id),
+    EnvironmentId int not null,
+    DeleteIncidents bit not null default 0
+);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v31.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v31.sql
new file mode 100644
index 00000000..a0f69733
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v31.sql
@@ -0,0 +1,3 @@
+alter table IncidentFeedback alter column RemoteAddress varchar(45) not null;
+alter table ErrorOrigins alter column IpAddress varchar(45) null;
+alter table WhitelistedDomainIps alter column IpAddress varchar(45) not null;
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v32.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v32.sql
new file mode 100644
index 00000000..e844159e
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v32.sql
@@ -0,0 +1 @@
+alter table Incidents add EscalationState int not null default(0);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v33.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v33.sql
new file mode 100644
index 00000000..6deb6d6a
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v33.sql
@@ -0,0 +1,17 @@
+create table InsightDefinitions
+(
+    Id int not null identity primary key,
+    Name varchar(40) not null,
+    DisplayName varchar(40) not null,
+    Description varchar(2500) not null
+);
+
+create table dbo.InsightsConfigurations
+(
+    Id int not null identity primary key,
+    DefinitionId int not null constraint FK_InsightsConfigurations_InsightDefinitions foreign key references InsightDefinitions(Id),
+    ApplicationId int not null constraint FK_InsightsConfigurations_Applications foreign key references Applications(Id) on delete CASCADE,
+    AccountId int not null,
+    FromDays int not null,
+    ToDays int not null
+);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v34.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v34.sql
new file mode 100644
index 00000000..bdc3c008
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v34.sql
@@ -0,0 +1,23 @@
+-- need to check if it's been part of commercial editions
+if not exists (select * from sysobjects where name='ApplicationGroups' and xtype='U')
+begin
+
+    CREATE TABLE dbo.ApplicationGroups
+    (
+        Id int not null identity primary key,
+        Name varchar(50) not null
+    );
+
+    CREATE Table dbo.ApplicationGroupMap
+    (
+        Id int not null identity primary key,
+        ApplicationId int not null constraint FK_ApplicationGroupMap_Application foreign key references Applications(id) on delete cascade,
+        ApplicationGroupId int not null constraint FK_ApplicationGroupMap_ApplicationGroup foreign key references ApplicationGroups(id),
+    );
+
+    insert into ApplicationGroups (Name) VALUES('General');
+    insert into ApplicationGroupMap (ApplicationId, ApplicationGroupId)
+    SELECT Id, 1
+    FROM Applications;
+
+end
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v35.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v35.sql
new file mode 100644
index 00000000..66e1d49f
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v35.sql
@@ -0,0 +1,6 @@
+create table UserSettings
+(
+    AccountId int not null primary key,
+    Name varchar(40)  not null,
+    Value varchar(max) not null
+);
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v36.sql b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v36.sql
new file mode 100644
index 00000000..fce6bffd
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/Coderr.v36.sql
@@ -0,0 +1,15 @@
+-- need to check if it's been part of commercial editions
+if not exists (select * from sysobjects where name='ErrorReportLogs' and xtype='U')
+begin
+
+CREATE Table dbo.ErrorReportLogs
+(
+    Id int not null identity primary key,
+    ReportId int not null constraint FK_ErrorReportLogs_ErrorReports foreign key references ErrorReports(id) on delete cascade,
+    IncidentId int not null constraint FK_ErrorReportLogs_Incidents foreign key references Incidents(id),
+    Json varchar(max) not null
+);
+
+
+
+end
diff --git a/src/Server/Coderr.Server.SqlServer/Schema/CoderrMigrationPointer.cs b/src/Server/Coderr.Server.SqlServer/Schema/CoderrMigrationPointer.cs
new file mode 100644
index 00000000..0a9872fa
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Schema/CoderrMigrationPointer.cs
@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Coderr.Server.SqlServer.Schema
+{
+    public class CoderrMigrationPointer
+    {
+    }
+}
diff --git a/src/Server/Coderr.Server.SqlServer/SqlServerTools.cs b/src/Server/Coderr.Server.SqlServer/SqlServerTools.cs
new file mode 100644
index 00000000..bead61a9
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/SqlServerTools.cs
@@ -0,0 +1,189 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Data.SqlClient;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Coderr.Client;
+using Coderr.Server.Infrastructure;
+using Coderr.Server.SqlServer.Migrations;
+
+namespace Coderr.Server.SqlServer
+{
+    /// 
+    ///     MS Sql Server specific implementation of the database tools.
+    /// 
+    /// 
+    /// 
+    /// These tools should only be used during setup and updates.
+    /// 
+    /// 
+    public class SqlServerTools : ISetupDatabaseTools
+    {
+        private readonly Func _connectionFactory;
+        private static readonly List _runners = new List();
+
+        public SqlServerTools(Func connectionFactory)
+        {
+            _connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
+        }
+
+        public static void AddMigrationRunner(MigrationRunner runner)
+        {
+            if (runner == null) throw new ArgumentNullException(nameof(runner));
+            if (_runners.Any(x => x.MigrationName == runner.MigrationName))
+            {
+                return;
+            }
+
+            _runners.Add(runner);
+        }
+
+        private bool IsTablesInstalled(IDbConnection connection)
+        {
+            using (var cmd = connection.CreateCommand())
+            {
+                cmd.CommandText = "SELECT OBJECT_ID(N'dbo.[Accounts]', N'U')";
+                var result = cmd.ExecuteScalar();
+
+                //null for SQL Express and DbNull for SQL Server
+                return result != null && !(result is DBNull);
+            }
+        }
+
+        /// 
+        ///     Checks if the tables exists and are for the current DB schema.
+        /// 
+        public bool IsTablesInstalled()
+        {
+            try
+            {
+                using (var con = _connectionFactory())
+                {
+                    return IsTablesInstalled(con);
+                }
+            }
+            catch (Exception)
+            {
+                return false;
+            }
+
+        }
+
+        [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities",
+            Justification = "Installation import = control over SQL")]
+        public void CreateTables()
+        {
+            foreach (var runner in _runners)
+            {
+                runner.Run();
+            }
+        }
+
+        IDbConnection ISetupDatabaseTools.OpenConnection()
+        {
+            return _connectionFactory();
+        }
+
+        private const string ConnectionTimeout = "Connection Timeout";
+        private const string ConnectTimeout = "Connect Timeout";
+
+        public void TestConnection(string connectionString)
+        {
+            if (connectionString == null) throw new ArgumentNullException(nameof(connectionString));
+
+            connectionString = ChangeConnectionTimeout(connectionString);
+
+            var con = new SqlConnection(connectionString);
+            con.Open();
+            con.Dispose();
+        }
+
+        private static string ChangeConnectionTimeout(string connectionString)
+        {
+            // both are valid.
+            var pos = connectionString.IndexOf(ConnectionTimeout, StringComparison.OrdinalIgnoreCase);
+            if (pos == -1)
+                pos = connectionString.IndexOf(ConnectTimeout, StringComparison.OrdinalIgnoreCase);
+
+            if (pos != -1)
+            {
+                var valueStart =
+                    connectionString.IndexOf("=", pos, StringComparison.OrdinalIgnoreCase);
+                var valueEnd = connectionString.IndexOf(';', valueStart);
+                connectionString = connectionString.Substring(0, valueStart) + "=5";
+                if (valueEnd != -1 && valueEnd < connectionString.Length)
+                {
+                    connectionString = connectionString.Substring(0, valueStart) + "=5" +
+                                       connectionString.Substring(valueEnd);
+                }
+                else
+                {
+                    connectionString = connectionString.Substring(0, valueStart) + "=5";
+                }
+            }
+            else
+            {
+                connectionString = connectionString.TrimEnd(';') + ";" + ConnectionTimeout + "=5;";
+            }
+
+            return connectionString;
+        }
+
+
+        public void MarkConfigurationAsComplete()
+        {
+            using (var con = _connectionFactory())
+            {
+                using (var cmd = con.CreateCommand())
+                {
+                    cmd.CommandText =
+                        "DELETE FROM Settings WHERE Section='Core' AND Name='Installation'";
+                    cmd.ExecuteNonQuery();
+                }
+
+                using (var cmd = con.CreateCommand())
+                {
+                    cmd.CommandText =
+                        "INSERT INTO Settings (Section, Name, Value) VALUES('Core', 'Installation', 'Complete')";
+                    cmd.ExecuteNonQuery();
+                }
+            }
+        }
+
+        public bool IsConfigurationComplete(string connectionString)
+        {
+            try
+            {
+                TestConnection(connectionString);
+            }
+            catch
+            {
+                return false;
+            }
+
+            try
+            {
+                using (var con = _connectionFactory())
+                {
+                    if (!IsTablesInstalled(con))
+                        return false;
+
+                    using (var cmd = con.CreateCommand())
+                    {
+                        cmd.CommandText =
+                            "SELECT Value FROM Settings WHERE Section='Core' AND Name='Installation'";
+                        var value = cmd.ExecuteScalar();
+                        return value?.Equals("Complete") == true;
+                    }
+                }
+            }
+            catch (Exception ex)
+            {
+                Err.Report(ex, connectionString);
+            }
+
+            return false;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Tools/ConnectionStringHelper.cs b/src/Server/Coderr.Server.SqlServer/Tools/ConnectionStringHelper.cs
new file mode 100644
index 00000000..16b6af5d
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Tools/ConnectionStringHelper.cs
@@ -0,0 +1,23 @@
+//using System;
+//using System.Configuration;
+
+//namespace Coderr.Server.SqlServer.Tools
+//{
+//    public static class ConnectionStringHelper
+//    {
+//        public static ConnectionStringSettings GetConnectionString()
+//        {
+//            var connectionStringName = ConfigurationManager.AppSettings["ConnectionStringName"] ?? "Db";
+
+//            var connectionString = ConfigurationManager.ConnectionStrings[connectionStringName];
+
+//            var environmentConnectionString = Environment.GetEnvironmentVariable("coderr_ConnectionString");
+//            if (!string.IsNullOrEmpty(environmentConnectionString))
+//            {
+//                connectionString = new ConnectionStringSettings(connectionStringName, environmentConnectionString, connectionString.ProviderName);
+//            }
+
+//            return connectionString;
+//        }
+//    }
+//}
diff --git a/src/Server/OneTrueError.SqlServer/Tools/DbCommandExtensions.cs b/src/Server/Coderr.Server.SqlServer/Tools/DbCommandExtensions.cs
similarity index 93%
rename from src/Server/OneTrueError.SqlServer/Tools/DbCommandExtensions.cs
rename to src/Server/Coderr.Server.SqlServer/Tools/DbCommandExtensions.cs
index 587dbe41..91699721 100644
--- a/src/Server/OneTrueError.SqlServer/Tools/DbCommandExtensions.cs
+++ b/src/Server/Coderr.Server.SqlServer/Tools/DbCommandExtensions.cs
@@ -1,23 +1,23 @@
-using System.Data;
-using System.Diagnostics.CodeAnalysis;
-
-namespace OneTrueError.SqlServer.Tools
-{
-    [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities",
-        Justification = "Invoker have control over the CommandText.")]
-    public static class DbCommandExtensions
-    {
-        [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
-        public static void Limit(this IDbCommand cmd, int count)
-        {
-            cmd.CommandText += string.Format(" OFFSET 0 ROWS FETCH NEXT {0} ROWS ONLY", count);
-        }
-
-        [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
-        public static void Paging(this IDbCommand cmd, int pageNumber, int pageSize)
-        {
-            var offset = (pageNumber - 1)*pageSize;
-            cmd.CommandText += string.Format(" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize);
-        }
-    }
+using System.Data;
+using System.Diagnostics.CodeAnalysis;
+
+namespace Coderr.Server.SqlServer.Tools
+{
+    [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities",
+        Justification = "Invoker have control over the CommandText.")]
+    public static class DbCommandExtensions
+    {
+        [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
+        public static void Limit(this IDbCommand cmd, int count)
+        {
+            cmd.CommandText += string.Format(" OFFSET 0 ROWS FETCH NEXT {0} ROWS ONLY", count);
+        }
+
+        [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")]
+        public static void Paging(this IDbCommand cmd, int pageNumber, int pageSize)
+        {
+            var offset = (pageNumber - 1)*pageSize;
+            cmd.CommandText += string.Format(" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Tools/EntitySerializer.cs b/src/Server/Coderr.Server.SqlServer/Tools/EntitySerializer.cs
similarity index 88%
rename from src/Server/OneTrueError.SqlServer/Tools/EntitySerializer.cs
rename to src/Server/Coderr.Server.SqlServer/Tools/EntitySerializer.cs
index ce5b5344..d02310bd 100644
--- a/src/Server/OneTrueError.SqlServer/Tools/EntitySerializer.cs
+++ b/src/Server/Coderr.Server.SqlServer/Tools/EntitySerializer.cs
@@ -1,45 +1,47 @@
-using System;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Converters;
-using OneTrueError.Infrastructure;
-
-namespace OneTrueError.SqlServer.Tools
-{
-    /// 
-    ///     Used for serialization of DB entities (typically value objects or child entities)v.
-    /// 
-    public class EntitySerializer
-    {
-        /// 
-        /// 
-        /// 
-        /// DBNull or string
-        /// 
-        public static T Deserialize(object json)
-        {
-            if (json is DBNull)
-                return default(T);
-
-            var settings = new JsonSerializerSettings
-            {
-                ContractResolver = new IncludeNonPublicMembersContractResolver(),
-                ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
-            };
-            //settings.Converters.Add(new DomainTriggerRuleJsonConverter());
-            settings.Converters.Add(new StringEnumConverter());
-            return JsonConvert.DeserializeObject((string) json, settings);
-        }
-
-        public static string Serialize(object dto)
-        {
-            var jsonSerializerSettings = new JsonSerializerSettings
-            {
-                ContractResolver = new IncludeNonPublicMembersContractResolver(),
-                TypeNameHandling = TypeNameHandling.Objects
-            };
-            jsonSerializerSettings.Converters.Add(new StringEnumConverter());
-            return JsonConvert.SerializeObject(dto, typeof(object),
-                jsonSerializerSettings);
-        }
-    }
+using System;
+using Coderr.Server.Infrastructure.Messaging;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace Coderr.Server.SqlServer.Tools
+{
+    /// 
+    ///     Used for serialization of DB entities (typically value objects or child entities)v.
+    /// 
+    public class EntitySerializer
+    {
+        /// 
+        /// 
+        /// 
+        /// DBNull or string
+        /// 
+        public static T Deserialize(object json)
+        {
+            if (json is DBNull)
+                return default(T);
+
+            var settings = new JsonSerializerSettings
+            {
+                NullValueHandling = NullValueHandling.Ignore,
+                ContractResolver = new IncludeNonPublicMembersContractResolver(),
+                ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor
+            };
+            //settings.Converters.Add(new DomainTriggerRuleJsonConverter());
+            settings.Converters.Add(new StringEnumConverter());
+            return JsonConvert.DeserializeObject((string) json, settings);
+        }
+
+        public static string Serialize(object dto)
+        {
+            var jsonSerializerSettings = new JsonSerializerSettings
+            {
+                NullValueHandling = NullValueHandling.Ignore,
+                ContractResolver = new IncludeNonPublicMembersContractResolver(),
+                TypeNameHandling = TypeNameHandling.Objects
+            };
+            jsonSerializerSettings.Converters.Add(new StringEnumConverter());
+            return JsonConvert.SerializeObject(dto, typeof(object),
+                jsonSerializerSettings);
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/UnitOfWorkWithTransaction.cs b/src/Server/Coderr.Server.SqlServer/UnitOfWorkWithTransaction.cs
new file mode 100644
index 00000000..a76856c1
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/UnitOfWorkWithTransaction.cs
@@ -0,0 +1,84 @@
+using System;
+using System.Data;
+using System.Data.Common;
+using System.Data.SqlClient;
+using Coderr.Server.Abstractions;
+using Griffin;
+using Griffin.Data;
+using log4net;
+
+namespace Coderr.Server.SqlServer
+{
+    /// 
+    ///     Required for background jobs which uses
+    /// 
+    public class UnitOfWorkWithTransaction : IAdoNetUnitOfWork, IGotTransaction
+    {
+        private readonly ILog _logger = LogManager.GetLogger(typeof(UnitOfWorkWithTransaction));
+        private DbCommand _lastCommand;
+
+        public UnitOfWorkWithTransaction(DbTransaction transaction)
+        {
+            Transaction = transaction ?? throw new ArgumentNullException(nameof(transaction));
+        }
+
+        public DbTransaction Transaction { get; private set; }
+
+        public void Dispose()
+        {
+            if (Transaction == null)
+                return;
+
+            if (_lastCommand != null)
+                _logger.Debug($"Rolling back {GetHashCode()}, last command: {_lastCommand.CommandText}");
+            else
+                _logger.Info($"Rolling back {GetHashCode()}");
+
+            var connection = Transaction.Connection;
+            Transaction.Rollback();
+            Transaction.Dispose();
+            Transaction = null;
+
+            connection?.Dispose();
+        }
+
+        public void SaveChanges()
+        {
+            // Already committed.
+            // some scenarios requires early SaveChanges
+            // to prevent dead locks. 
+            // when there is time, find and eliminate the reason of the deadlocks :(
+            if (Transaction == null)
+                return;
+
+            var connection = Transaction.Connection;
+            Transaction.Commit();
+            Transaction.Dispose();
+            Transaction = null;
+            connection.Dispose();
+        }
+
+        public IDbCommand CreateCommand()
+        {
+            var cmd = Transaction.Connection.CreateCommand();
+            cmd.Transaction = Transaction;
+            _lastCommand = cmd;
+            return cmd;
+        }
+
+        public void Execute(string sql, object parameters)
+        {
+            using (var cmd = CreateCommand())
+            {
+                cmd.CommandText = sql;
+                var ps = parameters.ToDictionary();
+                foreach (var p in ps)
+                {
+                    cmd.AddParameter(p.Key, p.Value);
+                }
+
+                cmd.ExecuteNonQuery();
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs
similarity index 79%
rename from src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs
index 05f410eb..5ac54e27 100644
--- a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackHandler.cs
@@ -1,44 +1,43 @@
-using System.Linq;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.Api.Web.Feedback.Queries;
-
-namespace OneTrueError.SqlServer.Web.Feedback.Queries
-{
-    [Component]
-    public class GetApplicationFeedbackHandler :
-        IQueryHandler
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public GetApplicationFeedbackHandler(IAdoNetUnitOfWork unitOfWork)
-        {
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task ExecuteAsync(GetFeedbackForApplicationPage query)
-        {
-            using (var cmd = _unitOfWork.CreateCommand())
-            {
-                cmd.CommandText =
-                    @"select IncidentFeedback.Description Message, IncidentFeedback.EmailAddress, IncidentFeedback.CreatedAtUtc as WrittenAtUtc, 
-       Incidents.Description as IncidentName, IncidentId
-from IncidentFeedback
-join Incidents on (IncidentId = Incidents.Id)
-WHERE IncidentFeedback.ApplicationId = @appId
-";
-                cmd.AddParameter("appId", query.ApplicationId);
-                var items = await cmd.ToListAsync();
-                return new GetFeedbackForApplicationPageResult
-                {
-                    Items = items.Where(x => !string.IsNullOrEmpty(x.Message)).ToArray(),
-                    Emails =
-                        items.Where(x => !string.IsNullOrEmpty(x.EmailAddress)).Select(x => x.EmailAddress).ToList()
-                };
-            }
-        }
-    }
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Web.Feedback.Queries;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Web.Feedback.Queries
+{
+    public class GetApplicationFeedbackHandler :
+        IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetApplicationFeedbackHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetFeedbackForApplicationPage query)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    @"select IncidentFeedback.Description Message, IncidentFeedback.EmailAddress, IncidentFeedback.CreatedAtUtc as WrittenAtUtc, 
+       Incidents.Description as IncidentName, IncidentId
+from IncidentFeedback
+join Incidents on (IncidentId = Incidents.Id)
+WHERE IncidentFeedback.ApplicationId = @appId
+";
+                cmd.AddParameter("appId", query.ApplicationId);
+                var items = await cmd.ToListAsync();
+                return new GetFeedbackForApplicationPageResult
+                {
+                    Items = items.Where(x => !string.IsNullOrEmpty(x.Message)).ToArray(),
+                    Emails =
+                        items.Where(x => !string.IsNullOrEmpty(x.EmailAddress)).Select(x => x.EmailAddress).ToList()
+                };
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs
similarity index 82%
rename from src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs
rename to src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs
index 8e09f0bb..ff8428b4 100644
--- a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs
+++ b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetApplicationFeedbackResultItemMapper.cs
@@ -1,26 +1,26 @@
-using Griffin.Data.Mapper;
-using OneTrueError.Api.Web.Feedback.Queries;
-
-namespace OneTrueError.SqlServer.Web.Feedback.Queries
-{
-    public class GetApplicationFeedbackResultItemMapper : EntityMapper
-    {
-        public GetApplicationFeedbackResultItemMapper()
-        {
-            Property(x => x.EmailAddress);
-            Property(x => x.IncidentId);
-            Property(x => x.IncidentName)
-                .NotForCrud()
-                .ToPropertyValue(FirstLine);
-            Property(x => x.Message);
-            Property(x => x.WrittenAtUtc);
-        }
-
-        private string FirstLine(object arg)
-        {
-            var str = arg.ToString();
-            var pos = str.IndexOfAny(new[] {'\r', '\n'});
-            return pos != -1 ? str.Substring(0, pos) : str;
-        }
-    }
+using Coderr.Server.Api.Web.Feedback.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Web.Feedback.Queries
+{
+    public class GetApplicationFeedbackResultItemMapper : EntityMapper
+    {
+        public GetApplicationFeedbackResultItemMapper()
+        {
+            Property(x => x.EmailAddress);
+            Property(x => x.IncidentId);
+            Property(x => x.IncidentName)
+                .NotForCrud()
+                .ToPropertyValue(FirstLine);
+            Property(x => x.Message);
+            Property(x => x.WrittenAtUtc);
+        }
+
+        private string FirstLine(object arg)
+        {
+            var str = arg.ToString();
+            var pos = str.IndexOfAny(new[] {'\r', '\n'});
+            return pos != -1 ? str.Substring(0, pos) : str;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs
similarity index 84%
rename from src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs
index d9fed5db..a471f5b2 100644
--- a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetIncidentFeedbackItemsHandler.cs
@@ -1,61 +1,64 @@
-using System;
-using System.Collections.Generic;
-using System.Data.Common;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using OneTrueError.Api.Web.Feedback.Queries;
-
-namespace OneTrueError.SqlServer.Web.Feedback.Queries
-{
-    [Component]
-    public class GetIncidentFeedbackHandler : IQueryHandler
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public GetIncidentFeedbackHandler(IAdoNetUnitOfWork unitOfWork)
-        {
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task ExecuteAsync(GetIncidentFeedback query)
-        {
-            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
-            {
-                cmd.CommandText =
-                    "SELECT IncidentId, IncidentFeedback.CreatedAtUtc, EmailAddress, IncidentFeedback.Description" +
-                    " FROM IncidentFeedback" +
-                    " JOIN Incidents ON (Incidents.Id = IncidentFeedback.IncidentId)" +
-                    " WHERE IncidentId = @id";
-                cmd.AddParameter("id", query.IncidentId);
-                cmd.CommandText += " ORDER BY IncidentFeedback.CreatedAtUtc DESC";
-
-                using (var reader = await cmd.ExecuteReaderAsync())
-                {
-                    var emails = new List();
-                    var items = new List();
-                    while (reader.Read())
-                    {
-                        var description = Convert.ToString(reader["Description"]);
-                        var email = Convert.ToString(Convert.ToString(reader["EmailAddress"]));
-                        if (!string.IsNullOrEmpty(description))
-                        {
-                            var item = new GetIncidentFeedbackResultItem();
-                            item.EmailAddress = email;
-                            item.Message = description;
-                            item.WrittenAtUtc = (DateTime) reader["CreatedAtUtc"];
-                            items.Add(item);
-                        }
-                        if (!string.IsNullOrEmpty(email) && !emails.Contains(email))
-                        {
-                            emails.Add(email);
-                        }
-                    }
-
-                    return new GetIncidentFeedbackResult(items, emails);
-                }
-            }
-        }
-    }
+using System;
+using System.Collections.Generic;
+using System.Data.Common;
+using System.Threading.Tasks;
+using Coderr.Server.Api.Web.Feedback.Queries;
+using DotNetCqs;
+using Coderr.Server.ReportAnalyzer.Abstractions;
+using Griffin.Data;
+using log4net;
+
+namespace Coderr.Server.SqlServer.Web.Feedback.Queries
+{
+    public class GetIncidentFeedbackHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+        private ILog _logger = LogManager.GetLogger(typeof(GetIncidentFeedbackHandler));
+
+
+        public GetIncidentFeedbackHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetIncidentFeedback query)
+        {
+            using (var cmd = (DbCommand) _unitOfWork.CreateCommand())
+            {
+                cmd.CommandText =
+                    "SELECT IncidentId, IncidentFeedback.CreatedAtUtc, EmailAddress, IncidentFeedback.Description" +
+                    " FROM IncidentFeedback" +
+                    " JOIN Incidents ON (Incidents.Id = IncidentFeedback.IncidentId)" +
+                    " WHERE IncidentId = @id";
+                cmd.AddParameter("id", query.IncidentId);
+                cmd.CommandText += " ORDER BY IncidentFeedback.CreatedAtUtc DESC";
+
+                _logger.Info("** Retreiving");
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    var emails = new List();
+                    var items = new List();
+                    while (reader.Read())
+                    {
+                        var description = Convert.ToString(reader["Description"]);
+                        var email = Convert.ToString(Convert.ToString(reader["EmailAddress"]));
+                        if (!string.IsNullOrEmpty(description))
+                        {
+                            var item = new GetIncidentFeedbackResultItem();
+                            item.EmailAddress = email;
+                            item.Message = description;
+                            item.WrittenAtUtc = (DateTime) reader["CreatedAtUtc"];
+                            items.Add(item);
+                        }
+                        if (!string.IsNullOrEmpty(email) && !emails.Contains(email))
+                        {
+                            emails.Add(email);
+                        }
+                    }
+
+                    return new GetIncidentFeedbackResult(items, emails);
+                }
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs
similarity index 75%
rename from src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs
rename to src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs
index 0521777a..6becafaa 100644
--- a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs
+++ b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackHandler.cs
@@ -1,66 +1,63 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Security.Claims;
-using System.Threading.Tasks;
-using DotNetCqs;
-using Griffin.Container;
-using Griffin.Data;
-using Griffin.Data.Mapper;
-using OneTrueError.Api.Web.Feedback.Queries;
-using OneTrueError.Infrastructure.Security;
-
-namespace OneTrueError.SqlServer.Web.Feedback.Queries
-{
-    [Component]
-    public class GetOverviewFeedbackHandler :
-        IQueryHandler
-    {
-        private readonly IAdoNetUnitOfWork _unitOfWork;
-
-        public GetOverviewFeedbackHandler(IAdoNetUnitOfWork unitOfWork)
-        {
-            _unitOfWork = unitOfWork;
-        }
-
-        public async Task ExecuteAsync(GetFeedbackForDashboardPage query)
-        {
-            using (var cmd = _unitOfWork.CreateCommand())
-            {
-                var sql =
-                    @"select IncidentFeedback.Description Message, IncidentFeedback.EmailAddress, IncidentFeedback.CreatedAtUtc as WrittenAtUtc, 
-                        ApplicationId, Applications.Name ApplicationName
-                        from IncidentFeedback
-                        join Applications on (ApplicationId = Applications.Id)
-                        WHERE ApplicationId IN ({0})";
-
-                // roundtrip to int to prevent
-                // something getting a string into our claims
-                // since the code below would otherwise
-                // allow SQL injection 
-                var appIds = ClaimsPrincipal.Current
-                    .FindAll(x => x.Type == OneTrueClaims.Application)
-                    .Select(x => int.Parse(x.Value).ToString())
-                    .ToList();
-                if (appIds.Count == 0)
-                {
-                    return new GetFeedbackForDashboardPageResult
-                    {
-                        Items = new GetFeedbackForDashboardPageResultItem[0],
-                        Emails = new List(),
-                        TotalCount = 0
-                    };
-                }
-
-                cmd.CommandText = sql.Replace("{0}", string.Join(",", appIds));
-                //WHERE Description is not null AND datalength(message) > 0";
-                var items = await cmd.ToListAsync();
-                return new GetFeedbackForDashboardPageResult
-                {
-                    Items = items.Where(x => !string.IsNullOrEmpty(x.Message)).ToArray(),
-                    Emails =
-                        items.Where(x => !string.IsNullOrEmpty(x.EmailAddress)).Select(x => x.EmailAddress).ToList()
-                };
-            }
-        }
-    }
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Api.Web.Feedback.Queries;
+using DotNetCqs;
+using Griffin.Data;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Web.Feedback.Queries
+{
+    public class GetFeedbackForDashboardPageHandler :
+        IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetFeedbackForDashboardPageHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        public async Task HandleAsync(IMessageContext context, GetFeedbackForDashboardPage query)
+        {
+            using (var cmd = _unitOfWork.CreateCommand())
+            {
+                var sql =
+                    @"select IncidentFeedback.Description Message, IncidentFeedback.EmailAddress, IncidentFeedback.CreatedAtUtc as WrittenAtUtc, 
+                        ApplicationId, Applications.Name ApplicationName
+                        from IncidentFeedback
+                        join Applications on (ApplicationId = Applications.Id)
+                        WHERE ApplicationId IN ({0})";
+
+                // roundtrip to int to prevent
+                // something getting a string into our claims
+                // since the code below would otherwise
+                // allow SQL injection 
+                var appIds = context.Principal
+                    .FindAll(x => x.Type == CoderrClaims.Application)
+                    .Select(x => int.Parse(x.Value).ToString())
+                    .ToList();
+                if (appIds.Count == 0)
+                {
+                    return new GetFeedbackForDashboardPageResult
+                    {
+                        Items = new GetFeedbackForDashboardPageResultItem[0],
+                        Emails = new List(),
+                        TotalCount = 0
+                    };
+                }
+
+                cmd.CommandText = sql.Replace("{0}", string.Join(",", appIds));
+                //WHERE Description is not null AND datalength(message) > 0";
+                var items = await cmd.ToListAsync();
+                return new GetFeedbackForDashboardPageResult
+                {
+                    Items = items.Where(x => !string.IsNullOrEmpty(x.Message)).ToArray(),
+                    Emails =
+                        items.Where(x => !string.IsNullOrEmpty(x.EmailAddress)).Select(x => x.EmailAddress).ToList()
+                };
+            }
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs
new file mode 100644
index 00000000..02d7a333
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs
@@ -0,0 +1,9 @@
+using Coderr.Server.Api.Web.Feedback.Queries;
+using Griffin.Data.Mapper;
+
+namespace Coderr.Server.SqlServer.Web.Feedback.Queries
+{
+    public class GetOverviewFeedbackResultItemMapper : EntityMapper
+    {
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.SqlServer/Web/Overview/GetOverviewHandler.cs b/src/Server/Coderr.Server.SqlServer/Web/Overview/GetOverviewHandler.cs
new file mode 100644
index 00000000..84380f26
--- /dev/null
+++ b/src/Server/Coderr.Server.SqlServer/Web/Overview/GetOverviewHandler.cs
@@ -0,0 +1,344 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Coderr.Server.Abstractions.Security;
+using Coderr.Server.Api.Web.Overview.Queries;
+using Coderr.Server.Domain.Core.Incidents;
+using DotNetCqs;
+using Griffin.Data;
+
+namespace Coderr.Server.SqlServer.Web.Overview
+{
+    internal class GetOverviewHandler : IQueryHandler
+    {
+        private readonly IAdoNetUnitOfWork _unitOfWork;
+
+        public GetOverviewHandler(IAdoNetUnitOfWork unitOfWork)
+        {
+            _unitOfWork = unitOfWork;
+        }
+
+        private DateTime StartDateForHours =>
+            //since we want to 22 if time is 21:30
+            DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-22);
+
+        private string ApplicationIds { get; set; }
+
+        public async Task HandleAsync(IMessageContext context, GetOverview query)
+        {
+            if (query.NumberOfDays == 0)
+                query.NumberOfDays = 30;
+
+            var isSysAdmin = context.Principal.IsSysAdmin();
+            var gotApps = context.Principal.FindAll(x => x.Type == CoderrClaims.Application).Any();
+
+            var labels = query.IncludeChartData ? CreateTimeLabels(query) : new string[0];
+
+            if (!isSysAdmin && !gotApps)
+            {
+                return new GetOverviewResult()
+                {
+                    StatSummary = new OverviewStatSummary(),
+                    IncidentsPerApplication = new GetOverviewApplicationResult[0],
+                    TimeAxisLabels = labels
+                };
+            }
+
+            AssignApplicationIds(context, isSysAdmin);
+            if (!ApplicationIds.Any())
+                return new GetOverviewResult();
+
+            if (query.NumberOfDays == 1)
+                return await GetTodaysOverviewAsync(query);
+
+            var result = new GetOverviewResult { Days = query.NumberOfDays };
+            if (query.IncludeChartData)
+            {
+                await LoadChartData(query, result, labels);
+            }
+            
+
+            // Not in live
+            //using (var cmd = _unitOfWork.CreateCommand())
+            //{
+            //    var from = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
+            //    var to = DateTime.UtcNow;
+            //    cmd.CommandText =
+            //        "SELECT sum(NumberOfReports) FROM IgnoredReports WHERE  date >= @from ANd date <= @to";
+            //    cmd.AddParameter("from", from);
+            //    cmd.AddParameter("to", to);
+            //    var value = cmd.ExecuteScalar();
+            //    if (value != DBNull.Value)
+            //        result.MissedReports = (int) value;
+
+            //}
+            
+            await GetStatSummary(query, result);
+
+
+            return result;
+        }
+
+        private async Task LoadChartData(GetOverview query, GetOverviewResult result, string[] labels)
+        {
+            var apps = new Dictionary();
+            var startDate = DateTime.Today.AddDays(-query.NumberOfDays);
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = $@"select Applications.Id, Applications.Name, cte.Date, cte.Count
+FROM 
+(
+	select Incidents.ApplicationId , cast(Incidents.CreatedAtUtc as date) as Date, count(Incidents.Id) as Count
+	from Incidents WITH(READUNCOMMITTED)
+	where Incidents.CreatedAtUtc >= @minDate 
+    AND Incidents.CreatedAtUtc <= GetUtcDate()
+	AND Incidents.ApplicationId in ({ApplicationIds})
+	group by Incidents.ApplicationId, cast(Incidents.CreatedAtUtc as date)
+) cte
+right join applications on (applicationid=applications.id)
+
+;";
+
+
+                cmd.AddParameter("minDate", startDate);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var appId = reader.GetInt32(0);
+                        if (!apps.TryGetValue(appId, out var app))
+                        {
+                            app = new GetOverviewApplicationResult(reader.GetString(1), startDate,
+                                query.NumberOfDays + 1); //+1 for today
+                            apps[appId] = app;
+                        }
+
+                        //no stats at all for this app
+                        if (reader[2] is DBNull)
+                        {
+                            var startDate2 = DateTime.Today.AddDays(-query.NumberOfDays + 1);
+                            for (var i = 0; i < query.NumberOfDays; i++)
+                            {
+                                app.AddValue(startDate2.AddDays(i), 0);
+                            }
+                        }
+                        else
+                            app.AddValue(reader.GetDateTime(2), reader.GetInt32(3));
+                    }
+
+                    result.TimeAxisLabels = labels;
+                    result.IncidentsPerApplication = apps.Values.ToArray();
+                }
+            }
+        }
+
+        private void AssignApplicationIds(IMessageContext context, bool isSysAdmin)
+        {
+            if (isSysAdmin)
+            {
+                var appIds = new List();
+                using (var cmd = _unitOfWork.CreateCommand())
+                {
+                    cmd.CommandText = "SELECT id FROM Applications WITH(READUNCOMMITTED)";
+                    using (var reader = cmd.ExecuteReader())
+                    {
+                        while (reader.Read())
+                        {
+                            appIds.Add(reader.GetInt32(0));
+                        }
+                    }
+                }
+
+                ApplicationIds = string.Join(",", appIds);
+            }
+            else
+            {
+                var appIds = context.Principal
+                    .FindAll(x => x.Type == CoderrClaims.Application)
+                    .Select(x => int.Parse(x.Value).ToString())
+                    .ToList();
+                ApplicationIds = string.Join(",", appIds);
+            }
+        }
+
+        private static string[] CreateTimeLabels(GetOverview query)
+        {
+            var startDate = DateTime.Today.AddDays(-query.NumberOfDays);
+            var labels = new string[query.NumberOfDays + 1]; //+1 for today
+            for (var i = 0; i <= query.NumberOfDays; i++)
+            {
+                labels[i] = startDate.AddDays(i).ToString("yyyy-MM-dd");
+            }
+            return labels;
+        }
+
+        private async Task GetStatSummary(GetOverview query, GetOverviewResult result)
+        {
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = $@"select count(id), max(CreatedAtUtc) 
+from incidents With(READUNCOMMITTED)
+where CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND Incidents.ApplicationId IN ({ApplicationIds})
+AND Incidents.State <> {(int)IncidentState.Ignored} 
+AND Incidents.State <> {(int)IncidentState.Closed};
+
+select count(id), max(ReceivedAtUtc)
+from errorreports WITH(READUNCOMMITTED) 
+where CreatedAtUtc >= @minDate
+AND ApplicationId IN ({ApplicationIds})
+
+select count(distinct emailaddress) 
+from IncidentFeedback WITH(READUNCOMMITTED)
+where CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND ApplicationId IN ({ApplicationIds})
+AND emailaddress is not null
+AND DATALENGTH(emailaddress) > 0;
+
+select count(*) 
+from IncidentFeedback WITH(READUNCOMMITTED)
+where CreatedAtUtc >= @minDate
+AND CreatedAtUtc <= GetUtcDate()
+AND ApplicationId IN ({ApplicationIds})
+AND Description is not null
+AND DATALENGTH(Description) > 0;
+";
+                if (query.IncludePartitions)
+                {
+                    cmd.CommandText += @"
+select max(pd.Name), max(pd.PartitionKey), partitionid, count(distinct value)
+from ApplicationPartitionInsights  api
+join PartitionDefinitions pd on (pd.Id = api.PartitionId)
+where YearMonth = @yearMonth
+group by partitionId";
+                    cmd.AddParameter("yearMonth", new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1));
+                }
+
+                var minDate = query.NumberOfDays == 1
+                    ? StartDateForHours
+                    : DateTime.Today.AddDays(-query.NumberOfDays);
+                cmd.AddParameter("minDate", minDate);
+
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    if (!await reader.ReadAsync())
+                    {
+                        throw new InvalidOperationException("Expected to be able to read.");
+                    }
+
+                    var value = reader[1];
+                    var data = new OverviewStatSummary
+                    {
+                        Incidents = reader.GetInt32(0),
+                        NewestIncidentReceivedAtUtc = value is DBNull ? null : (DateTime?)value
+                    };
+                    await reader.NextResultAsync();
+                    await reader.ReadAsync();
+
+                    value = reader[1];
+                    data.Reports = reader.GetInt32(0);
+                    data.NewestReportReceivedAtUtc = value is DBNull ? null : (DateTime?)value;
+                    await reader.NextResultAsync();
+                    await reader.ReadAsync();
+                    
+                    data.Followers = reader.GetInt32(0);
+                    await reader.NextResultAsync();
+                    await reader.ReadAsync();
+                    
+                    data.UserFeedback = reader.GetInt32(0);
+
+                    if (query.IncludePartitions)
+                    {
+                        await reader.NextResultAsync();
+                        var partitions = new List();
+                        while (await reader.ReadAsync())
+                        {
+                            var item = new PartitionOverview
+                            {
+                                Name = reader.GetString(1),
+                                DisplayName = reader.GetString(0),
+                                Value = reader.GetInt32(3)
+                            };
+                            partitions.Add(item);
+                        }
+
+                        data.Partitions = partitions.ToArray();
+                    }
+                    
+                    result.StatSummary = data;
+                    
+                }
+            }
+        }
+
+        private async Task GetTodaysOverviewAsync(GetOverview query)
+        {
+            var result = new GetOverviewResult
+            {
+                TimeAxisLabels = new string[24]
+            };
+            var startDate = StartDateForHours;
+            var apps = new Dictionary();
+            for (var i = 0; i < 24; i++)
+            {
+                result.TimeAxisLabels[i] = startDate.AddHours(i).ToString("HH:mm");
+            }
+
+            using (var cmd = _unitOfWork.CreateDbCommand())
+            {
+                cmd.CommandText = $@"select Applications.Id, Applications.Name, cte.Date, cte.Count
+FROM 
+(
+	select Incidents.ApplicationId , DATEPART(HOUR, Incidents.CreatedAtUtc) as Date, count(Incidents.Id) as Count
+	from Incidents WITH(READUNCOMMITTED)
+	where Incidents.CreatedAtUtc >= @minDate
+    AND CreatedAtUtc <= GetUtcDate()
+    AND Incidents.ApplicationId IN ({ApplicationIds})
+	group by Incidents.ApplicationId, DATEPART(HOUR, Incidents.CreatedAtUtc)
+) cte
+right join applications WITH(READUNCOMMITTED) on (applicationid=applications.id)";
+
+
+                cmd.AddParameter("minDate", startDate);
+                using (var reader = await cmd.ExecuteReaderAsync())
+                {
+                    while (await reader.ReadAsync())
+                    {
+                        var appId = reader.GetInt32(0);
+                        if (!apps.TryGetValue(appId, out var app))
+                        {
+                            app = new GetOverviewApplicationResult(reader.GetString(1), startDate, 1);
+                            apps[appId] = app;
+                        }
+
+                        if (reader[2] is DBNull)
+                        {
+                            for (var i = 0; i < 24; i++)
+                            {
+                                app.AddValue(startDate.AddHours(i), 0);
+                            }
+                        }
+                        else
+                        {
+                            var hour = reader.GetInt32(2);
+                            app.AddValue(
+                                hour < DateTime.Now.AddHours(1).Hour //since we want 22:00 if time is 21:30
+                                    ? DateTime.Today.AddHours(hour)
+                                    : DateTime.Today.AddDays(-1).AddHours(hour), reader.GetInt32(3));
+                        }
+                    }
+
+                    result.IncidentsPerApplication = apps.Values.ToArray();
+                }
+            }
+
+            await GetStatSummary(query, result);
+
+            return result;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Web.Tests/App.config b/src/Server/Coderr.Server.Web.Tests/App.config
new file mode 100644
index 00000000..4022e283
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/App.config
@@ -0,0 +1,6 @@
+
+
+  
+    
+  
+
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Web.Tests/Coderr.Server.Web.Tests.csproj b/src/Server/Coderr.Server.Web.Tests/Coderr.Server.Web.Tests.csproj
new file mode 100644
index 00000000..295f5ce8
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Coderr.Server.Web.Tests.csproj
@@ -0,0 +1,55 @@
+
+
+  
+    net452
+    codeRR.Server.Web.Tests
+    codeRR.Server.Web.Tests
+  
+
+  
+    
+    
+    
+    
+    
+    
+  
+
+  
+    
+    
+  
+
+  
+    
+  
+
+  
+    
+      True
+      True
+      applicationhost.tt
+      Never
+    
+    
+      TextTemplatingFileGenerator
+      applicationhost.config
+    
+  
+
+  
+    
+  
+
+  
+    True
+    True
+  
+  
+  
+  
+
+
diff --git a/src/Server/Coderr.Server.Web.Tests/Helpers/Extensions/WebDriverExtensions.cs b/src/Server/Coderr.Server.Web.Tests/Helpers/Extensions/WebDriverExtensions.cs
new file mode 100644
index 00000000..8bc6221a
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Helpers/Extensions/WebDriverExtensions.cs
@@ -0,0 +1,62 @@
+using System;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Helpers.Extensions
+{
+    public static class WebDriverExtensions
+    {
+        public static bool WaitUntilElementIsPresent(this IWebDriver driver, By by, int timeout = 5)
+        {
+            var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeout));
+            return wait.Until(d => d.ElementIsPresent(by));
+        }
+
+        public static bool WaitUntilElementIsPresent(this IWebDriver driver, IWebElement element, int timeout = 5)
+        {
+            var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeout));
+            return wait.Until(d => d.ElementIsPresent(element));
+        }
+
+        public static string WaitUntilTitleEquals(this IWebDriver driver, string title, int timeout = 5)
+        {
+            try
+            {
+                var wait = new WebDriverWait(driver, TimeSpan.FromSeconds(timeout));
+                wait.Until(ExpectedConditions.TitleIs(title));
+            }
+            catch
+            {
+                // ignored
+            }
+
+            return driver.Title;
+        }
+
+        private static bool ElementIsPresent(this IWebDriver driver, By by)
+        {
+            var present = false;
+            try
+            {
+                present = driver.FindElement(by).Displayed;
+            }
+            catch (NoSuchElementException)
+            {
+            }
+            return present;
+        }
+
+        private static bool ElementIsPresent(this IWebDriver driver, IWebElement element)
+        {
+            var present = false;
+            try
+            {
+                present = element.Displayed;
+            }
+            catch (NoSuchElementException)
+            {
+            }
+            return present;
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Helpers/IisExpressHelper.cs b/src/Server/Coderr.Server.Web.Tests/Helpers/IisExpressHelper.cs
new file mode 100644
index 00000000..1638e1f1
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Helpers/IisExpressHelper.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading;
+
+namespace codeRR.Server.Web.Tests.Helpers
+{
+    public class IisExpressHelper
+    {
+        private Process _process;
+
+        /// 
+        ///     The application pool that IIS Express should use. Defaults to "Clr4IntegratedAppPool".
+        /// 
+        public string AppPool { get; set; }
+
+        public string BaseUrl => "http://localhost:50473/coderr/";
+
+        /// 
+        ///     Path to the IIS Express configuration file. Defaults to
+        ///     USER\DOCUMENTS\IISExpress\config\applicationhost.config.
+        /// 
+        public string ConfigPath { get; set; }
+
+        public Dictionary EnvironmentVariables { get; set; }
+
+        /// 
+        ///     The path to the iisexpress.exe executable. Defaults to the default installation path
+        ///     (e.g. C:\Program Files (x86)\IIS Express\iisexpress.exe), if not explicitly set.
+        /// 
+        public string ExePath { get; set; }
+
+        /// 
+        ///     Gets a value indicating whether the associated IIS Express instance is currently running.
+        /// 
+        public bool IsRunning => _process != null;
+
+        /// 
+        ///     Starts IIS Express with the specified website.
+        /// 
+        /// The website that IIS Express should host.
+        /// 
+        ///     Argument  is null.
+        /// 
+        /// 
+        ///     There already is an associated IIS Express instance running (i.e.  is true).
+        /// 
+        /// 
+        ///     Either  or  could not be found.
+        /// 
+        public void Start(string site)
+        {
+            if (site == null)
+                throw new ArgumentNullException("site");
+
+            if (_process != null)
+                throw new InvalidOperationException("IIS Express is already running.");
+
+            SetDefaultsWhereNecessary();
+
+            if (!File.Exists(ExePath))
+                throw new FileNotFoundException($"Path to IIS Express executable is invalid: '{ExePath}'.");
+
+            if (string.IsNullOrEmpty(ConfigPath) || !File.Exists(ConfigPath))
+                throw new FileNotFoundException($"Path to IIS Express configuration file is invalid: '{ConfigPath}'.");
+
+            var iisExpressThread = new Thread(() => StartIisExpress(p => _process = p, site)) {IsBackground = true};
+            iisExpressThread.Start();
+            //StartIisExpress(p => _process = p, site);
+
+            var attemptsLeft = 50;
+            while (attemptsLeft > 0 && _process == null)
+            {
+                Thread.Sleep(200);
+                attemptsLeft--;
+            }
+
+            if (_process == null)
+                throw new InvalidOperationException("Failed to start IIS express");
+        }
+
+        /// 
+        ///     Stops the IIS Express instance that was formerly started via the  method.
+        /// 
+        public void Stop()
+        {
+            if (_process != null)
+            {
+                _process.Kill();
+                _process.WaitForExit();
+                _process.Dispose();
+                _process = null;
+            }
+        }
+
+        private void SetDefaultsWhereNecessary()
+        {
+            if (string.IsNullOrEmpty(ExePath))
+                ExePath = Path.Combine(
+                    Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
+                    @"IIS Express\iisexpress.exe");
+
+            if (string.IsNullOrEmpty(ConfigPath))
+                ConfigPath = Path.Combine(
+                    Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+                    @"IISExpress\config\applicationhost.config");
+
+            if (string.IsNullOrEmpty(AppPool))
+                AppPool = "Clr4IntegratedAppPool";
+        }
+
+        private void StartIisExpress(Action action, string site)
+        {
+            var isRunning = false;
+
+            var process = new Process
+            {
+                StartInfo = new ProcessStartInfo
+                {
+                    FileName = ExePath,
+                    Arguments = $"/config:\"{Path.GetFullPath(ConfigPath)}\" /site:\"{site}\" /apppool:\"{AppPool}\"",
+                    RedirectStandardOutput = true,
+                    RedirectStandardInput = true,
+                    UseShellExecute = false,
+                    CreateNoWindow = true
+                }
+            };
+
+            if (EnvironmentVariables != null)
+            {
+                foreach (var environmentVariable in EnvironmentVariables)
+                {
+                    Console.WriteLine($"Setting environment variable '{environmentVariable.Key}' to '{environmentVariable.Value}'");
+
+                    process.StartInfo.EnvironmentVariables[environmentVariable.Key] = environmentVariable.Value;
+                }
+            }
+
+            process.OutputDataReceived += (sender, args) =>
+            {
+                Console.WriteLine(args.Data);
+
+                if (string.IsNullOrEmpty(args.Data))
+                    return;
+
+                if (args.Data.Contains("IIS Express is running"))
+                    isRunning = true;
+            };
+
+            process.Start();
+            process.BeginOutputReadLine();
+            
+            var attemptsLeft = 50;
+            while (attemptsLeft > 0 && !isRunning)
+            {
+                Thread.Sleep(200);
+                attemptsLeft--;
+            }
+            if (!isRunning)
+                throw new InvalidOperationException("Failed to receive data from IIS ex");
+           
+            action(process);
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/BrowserAttribute.cs b/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/BrowserAttribute.cs
new file mode 100644
index 00000000..a74fa8e9
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/BrowserAttribute.cs
@@ -0,0 +1,22 @@
+using System.Collections.Generic;
+using System.Reflection;
+using OpenQA.Selenium;
+using Xunit.Sdk;
+
+namespace codeRR.Server.Web.Tests.Helpers.Selenium
+{
+    public class BrowserAttribute : DataAttribute
+    {
+        private readonly IWebDriver _driver;
+
+        public BrowserAttribute(BrowserType browserType)
+        {
+            _driver = DriverFactory.Create(browserType);
+        }
+
+        public override IEnumerable GetData(MethodInfo testMethod)
+        {
+            return new[] {new[] {_driver}};
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/BrowserType.cs b/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/BrowserType.cs
new file mode 100644
index 00000000..db85d513
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/BrowserType.cs
@@ -0,0 +1,9 @@
+namespace codeRR.Server.Web.Tests.Helpers.Selenium
+{
+    public enum BrowserType
+    {
+        InternetExplorer,
+        Chrome,
+        Firefox
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/DriverFactory.cs b/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/DriverFactory.cs
new file mode 100644
index 00000000..569f5bc9
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Helpers/Selenium/DriverFactory.cs
@@ -0,0 +1,33 @@
+using System;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Chrome;
+using OpenQA.Selenium.IE;
+
+namespace codeRR.Server.Web.Tests.Helpers.Selenium
+{
+    public static class DriverFactory
+    {
+        public static IWebDriver Create(BrowserType browserType)
+        {
+            IWebDriver driver;
+
+            switch (browserType)
+            {
+                case BrowserType.Chrome:
+                    driver = new ChromeDriver();
+                    break;
+
+                case BrowserType.InternetExplorer:
+                    driver = new InternetExplorerDriver();
+                    break;
+
+                default:
+                    throw new ArgumentOutOfRangeException();
+            }
+
+            driver.Manage().Window.Maximize();
+
+            return driver;
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Helpers/xUnit/TestCaseOrderer.cs b/src/Server/Coderr.Server.Web.Tests/Helpers/xUnit/TestCaseOrderer.cs
new file mode 100644
index 00000000..6bbfa363
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Helpers/xUnit/TestCaseOrderer.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit.Abstractions;
+using Xunit.Sdk;
+
+namespace codeRR.Server.Web.Tests.Helpers.xUnit
+{
+    public class TestCaseOrderer : ITestCaseOrderer
+    {
+        public IEnumerable OrderTestCases(IEnumerable testCases)
+            where TTestCase : ITestCase
+        {
+            var sortedMethods = new SortedDictionary>();
+
+            foreach (var testCase in testCases)
+            {
+                var priority = 1;
+
+                foreach (var attr in testCase.TestMethod.Method.GetCustomAttributes(typeof(TestPriorityAttribute)
+                    .AssemblyQualifiedName))
+                    priority = attr.GetNamedArgument("Priority");
+
+                GetOrCreate(sortedMethods, priority).Add(testCase);
+            }
+
+            foreach (var list in sortedMethods.Keys.Select(priority => sortedMethods[priority]))
+            {
+                list.Sort((x, y) => StringComparer.OrdinalIgnoreCase.Compare(x.TestMethod.Method.Name,
+                    y.TestMethod.Method.Name));
+
+                foreach (var testCase in list)
+                    yield return testCase;
+            }
+        }
+
+        private static TValue GetOrCreate(IDictionary dictionary, TKey key)
+            where TValue : new()
+        {
+            if (dictionary.TryGetValue(key, out var result))
+                return result;
+
+            result = new TValue();
+            dictionary[key] = result;
+
+            return result;
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Helpers/xUnit/TestPriorityAttribute.cs b/src/Server/Coderr.Server.Web.Tests/Helpers/xUnit/TestPriorityAttribute.cs
new file mode 100644
index 00000000..2cbc87f6
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Helpers/xUnit/TestPriorityAttribute.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace codeRR.Server.Web.Tests.Helpers.xUnit
+{
+    public class TestPriorityAttribute : Attribute
+    {
+        public int Priority { get; set; }
+
+        public TestPriorityAttribute(int priority)
+        {
+            Priority = priority;
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/Account/ActivationRequestedPage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/Account/ActivationRequestedPage.cs
new file mode 100644
index 00000000..b3405055
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/Account/ActivationRequestedPage.cs
@@ -0,0 +1,17 @@
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Pages.Account
+{
+    public class ActivationRequestedPage : BasePage
+    {
+        public ActivationRequestedPage(IWebDriver webDriver) : base(webDriver, "Account/ActivationRequested", "Account registered - codeRR")
+        {
+        }
+
+        public void VerifyIsCurrentPage()
+        {
+            Wait.Until(ExpectedConditions.TitleIs(Title));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/Account/LoginPage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/Account/LoginPage.cs
new file mode 100644
index 00000000..b7ce807b
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/Account/LoginPage.cs
@@ -0,0 +1,127 @@
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.PageObjects;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Pages.Account
+{
+    public class LoginPage : BasePage
+    {
+        public LoginPage(IWebDriver webDriver) : base(webDriver, "Account/Login", "Login - codeRR")
+        {
+        }
+
+        [FindsBy(How = How.Id, Using = "login-button")]
+        public IWebElement SignInButton { get; set; }
+
+        [FindsBy(How = How.Id, Using = "Username")]
+        public IWebElement UserNameField { get; set; }
+
+        [FindsBy(How = How.Id, Using = "Password")]
+        public IWebElement PasswordField { get; set; }
+
+        public IPage LoginWithValidCredentials(string userName, string password)
+        {
+            NavigateToPage();
+
+            UserNameField.Clear();
+            UserNameField.SendKeys(userName);
+
+            PasswordField.Clear();
+            PasswordField.SendKeys(password);
+
+            SignInButton.Click();
+
+            return PageHelper.ResolvePage(WebDriver);
+        }
+
+        public LoginPage LoginWithNonExistingUserWithoutPasswordSpecified()
+        {
+            NavigateToPage();
+
+            UserNameField.Clear();
+            UserNameField.SendKeys("NonExistingUsername");
+
+            PasswordField.Clear();
+
+            SignInButton.Click();
+
+            return this;
+        }
+
+        public LoginPage LoginWithNonExistingUserWithPasswordSpecified()
+        {
+            NavigateToPage();
+
+            UserNameField.Clear();
+            UserNameField.SendKeys("NonExistingUsername");
+
+            PasswordField.Clear();
+            PasswordField.SendKeys(TestUser.Password);
+
+            SignInButton.Click();
+
+            return this;
+        }
+
+        public LoginPage LoginWithNoUserNameSpecified()
+        {
+            NavigateToPage();
+
+            UserNameField.Clear();
+
+            PasswordField.Clear();
+            PasswordField.SendKeys(TestUser.Password);
+
+            SignInButton.Click();
+
+            return this;
+        }
+
+        public LoginPage LoginWithNoUserNameAndNoPasswordSpecified()
+        {
+            NavigateToPage();
+
+            UserNameField.Clear();
+
+            PasswordField.Clear();
+
+            SignInButton.Click();
+
+            return this;
+        }
+
+        public LoginPage LoginWithNoPasswordSpecified()
+        {
+            NavigateToPage();
+
+            UserNameField.Clear();
+            UserNameField.SendKeys(TestUser.Username);
+
+            PasswordField.Clear();
+
+            SignInButton.Click();
+
+            return this;
+        }
+
+        public LoginPage LoginWithWrongPasswordSpecified()
+        {
+            NavigateToPage();
+
+            UserNameField.Clear();
+            UserNameField.SendKeys(TestUser.Username);
+
+            PasswordField.Clear();
+            PasswordField.SendKeys(TestUser.Password.Substring(1));
+
+            SignInButton.Click();
+
+            return this;
+        }
+
+        public void VerifyIsCurrentPage()
+        {
+            Wait.Until(ExpectedConditions.TitleIs(Title));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/Account/LogoutPage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/Account/LogoutPage.cs
new file mode 100644
index 00000000..732e0066
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/Account/LogoutPage.cs
@@ -0,0 +1,18 @@
+using OpenQA.Selenium;
+
+namespace codeRR.Server.Web.Tests.Pages.Account
+{
+    public class LogoutPage : BasePage
+    {
+        public LogoutPage(IWebDriver webDriver) : base(webDriver, "Account/Logout", "")
+        {
+        }
+
+        public LoginPage Logout()
+        {
+            NavigateToPage(false);
+
+            return new LoginPage(WebDriver);
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/Account/RegisterPage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/Account/RegisterPage.cs
new file mode 100644
index 00000000..a1ba49ad
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/Account/RegisterPage.cs
@@ -0,0 +1,105 @@
+using codeRR.Server.Web.Tests.Helpers.Extensions;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.PageObjects;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Pages.Account
+{
+    public class RegisterPage : BasePage
+    {
+        public RegisterPage(IWebDriver webDriver) : base(webDriver, "Account/Register", "Register account - codeRR")
+        {
+        }
+
+        [FindsBy(How = How.Id, Using = "UserName")]
+        public IWebElement UserNameField { get; set; }
+
+        [FindsBy(How = How.Id, Using = "Password")]
+        public IWebElement PasswordField { get; set; }
+
+        [FindsBy(How = How.Id, Using = "Password2")]
+        public IWebElement RetypePasswordField { get; set; }
+
+        [FindsBy(How = How.Id, Using = "Email")]
+        public IWebElement EmailField { get; set; }
+
+        [FindsBy(How = How.ClassName, Using = "btn-primary")]
+        public IWebElement SignUpButton { get; set; }
+
+        [FindsBy(How = How.ClassName, Using = "field-validation-error")]
+        public IWebElement ValidationErrorField { get; set; }
+
+        public void VerifyIsCurrentPage()
+        {
+            Wait.Until(ExpectedConditions.TitleIs(Title));
+        }
+
+        public RegisterPage RegisterUsingAlreadyTakenUsername()
+        {
+            NavigateToPage();
+
+            ClearForm();
+
+            UserNameField.SendKeys(TestUser.Username);
+            PasswordField.SendKeys(TestUser.Password);
+            RetypePasswordField.SendKeys(TestUser.Password);
+            EmailField.SendKeys(TestUser.Email);
+
+            SignUpButton.Click();
+
+            return this;
+        }
+
+        public RegisterPage RegisterUsingAlreadyTakenEmail()
+        {
+            NavigateToPage();
+
+            ClearForm();
+
+            UserNameField.SendKeys(TestUser.Username + "2");
+            PasswordField.SendKeys(TestUser.Password);
+            RetypePasswordField.SendKeys(TestUser.Password);
+            EmailField.SendKeys(TestUser.Email);
+
+            SignUpButton.Click();
+
+            return this;
+        }
+
+        public ActivationRequestedPage RegisterNewUser()
+        {
+            NavigateToPage();
+
+            ClearForm();
+
+            UserNameField.SendKeys(TestUser.Username + "2");
+            PasswordField.SendKeys(TestUser.Password);
+            RetypePasswordField.SendKeys(TestUser.Password);
+            EmailField.SendKeys("TestUser2@coderrapp.com");
+
+            SignUpButton.Click();
+
+            return new ActivationRequestedPage(WebDriver);
+        }
+
+        public RegisterPage VerifyUsernameIsAlreadyTaken()
+        {
+            WebDriver.WaitUntilElementIsPresent(ValidationErrorField);
+            return this;
+        }
+
+        public RegisterPage VerifyEmailIsAlreadyTaken()
+        {
+            WebDriver.WaitUntilElementIsPresent(ValidationErrorField);
+            return this;
+        }
+
+        private void ClearForm()
+        {
+            UserNameField.Clear();
+            PasswordField.Clear();
+            RetypePasswordField.Clear();
+            EmailField.Clear();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/ApplicationPage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/ApplicationPage.cs
new file mode 100644
index 00000000..ca6ea3e5
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/ApplicationPage.cs
@@ -0,0 +1,29 @@
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.PageObjects;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Pages
+{
+    public class ApplicationPage : BasePage
+    {
+        public ApplicationPage(IWebDriver webDriver, int id) : base(webDriver, "#/application/{id}", "")
+        {
+            Url = Url.Replace("{id}", id.ToString());
+        }
+
+        [FindsBy(How = How.Id, Using = "pageTitle")]
+        public IWebElement PageTitle { get; set; }
+
+        public void VerifyIsCurrentPage()
+        {
+            Wait.Until(ExpectedConditions.TitleIs(Title));
+        }
+
+        public void VerifyIncidentReported()
+        {
+            var by = By.PartialLinkText("Value cannot be null");
+            //var element = WebDriver.FindElement(by);
+            Wait.Until(ExpectedConditions.ElementExists(by));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/BasePage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/BasePage.cs
new file mode 100644
index 00000000..27d7f3be
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/BasePage.cs
@@ -0,0 +1,57 @@
+using System;
+using codeRR.Server.SqlServer.Tests.Models;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.PageObjects;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Pages
+{
+    public class BasePage : IPage
+    {
+        protected IWebDriver WebDriver;
+        protected WebDriverWait Wait;
+        protected string BaseUrl { get; }
+        public string Url { get; set; }
+        public string Title { get; }
+
+        protected readonly TestUser TestUser = WebTest.TestUser;
+
+        public BasePage(IWebDriver webDriver, string url, string title)
+        {
+            WebDriver = webDriver;
+            Title = title;
+
+            Wait = new WebDriverWait(webDriver, TimeSpan.FromSeconds(5));
+
+            BaseUrl = "http://localhost:50473/coderr/";
+
+            Url = new Uri(new Uri(BaseUrl), url).ToString();
+
+            PageFactory.InitElements(WebDriver, this);
+        }
+
+        public void NavigateToPage(bool wait = true)
+        {
+            WebDriver.Navigate().GoToUrl(Url);
+
+            if(wait)
+                Wait.Until(ExpectedConditions.UrlContains(Url));
+        }
+
+        public void WaitForTitle(string title)
+        {
+            Wait.Until(ExpectedConditions.TitleIs(title));
+        }
+
+        public void DeleteCookies()
+        {
+            WebDriver.Manage().Cookies.DeleteAllCookies();
+        }
+
+        public void Close()
+        {
+            WebDriver.Close();
+            WebDriver.Dispose();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/ConfigureApplicationPage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/ConfigureApplicationPage.cs
new file mode 100644
index 00000000..bf777de9
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/ConfigureApplicationPage.cs
@@ -0,0 +1,41 @@
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.PageObjects;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Pages
+{
+    public class ConfigureApplicationPage : BasePage
+    {
+        public ConfigureApplicationPage(IWebDriver webDriver) : base(webDriver, "configure/application", "Create a new application - codeRR")
+        {
+        }
+
+        [FindsBy(How = How.ClassName, Using = "btn-primary")]
+        public IWebElement CreateButton { get; set; }
+
+        [FindsBy(How = How.Name, Using = "Name")]
+        public IWebElement ApplicationNameField { get; set; }
+
+        public ConfigureApplicationPage CreateApplication(string name)
+        {
+            NavigateToPage();
+
+            ApplicationNameField.Clear();
+            ApplicationNameField.SendKeys(name);
+
+            CreateButton.Click();
+
+            return this;
+        }
+
+        public void VerifyIsCurrentPage()
+        {
+            Wait.Until(ExpectedConditions.TitleIs(Title));
+        }
+
+        public void VerifySuccessfullyCreatedApplication(string name)
+        {
+            Wait.Until(ExpectedConditions.TitleIs(name));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/HomePage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/HomePage.cs
new file mode 100644
index 00000000..0c8f3fa2
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/HomePage.cs
@@ -0,0 +1,49 @@
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.PageObjects;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Pages
+{
+    public class HomePage : BasePage
+    {
+        public HomePage(IWebDriver webDriver) : base(webDriver, "", "Overview")
+        {
+        }
+
+        [FindsBy(How = How.XPath, Using = "//a/span[.=' MyTestApp ']")]
+        public IWebElement NavigationMyTestApp { get; set; }
+
+        [FindsBy(How = How.Id, Using = "pageTitle")]
+        public IWebElement PageTitle { get; set; }
+
+        [FindsBy(How = How.XPath, Using = "//a/span[.=' Dashboard ']")]
+        public IWebElement NavigationDashboard { get; set; }
+
+        [FindsBy(How = How.XPath, Using = "//a[.='Overview']")]
+        public IWebElement NavigationDashboardOverview { get; set; }
+
+        [FindsBy(How = How.XPath, Using = "//a[.='Incidents']")]
+        public IWebElement NavigationDashboardIncidents { get; set; }
+
+        [FindsBy(How = How.XPath, Using = "//a[.='Feedback']")]
+        public IWebElement NavigationDashboardFeedback { get; set; }
+
+        public bool HasApplicationInNavigation(string applicationName)
+        {
+            var by = By.XPath($"//a/span[.=' {applicationName} ']");
+            var element = WebDriver.FindElement(by);
+
+            return true;
+        }
+
+        public void VerifyIsCurrentPage()
+        {
+            Wait.Until(ExpectedConditions.TitleIs("Overview"));
+        }
+
+        public void VerifyNavigatedToMyTestApp()
+        {
+            Wait.Until(ExpectedConditions.TextToBePresentInElement(PageTitle, "MyTestApp"));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/IPage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/IPage.cs
new file mode 100644
index 00000000..e7e7ba73
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/IPage.cs
@@ -0,0 +1,8 @@
+namespace codeRR.Server.Web.Tests.Pages
+{
+    public interface IPage
+    {
+        string Url { get; set; }
+        string Title { get; }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/IncidentsPage.cs b/src/Server/Coderr.Server.Web.Tests/Pages/IncidentsPage.cs
new file mode 100644
index 00000000..22a503c5
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/IncidentsPage.cs
@@ -0,0 +1,29 @@
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.PageObjects;
+using OpenQA.Selenium.Support.UI;
+
+namespace codeRR.Server.Web.Tests.Pages
+{
+    public class IncidentsPage : BasePage
+    {
+        public IncidentsPage(IWebDriver webDriver, int id) : base(webDriver, "#/application/{id}/incidents/", "Incidents")
+        {
+            Url = Url.Replace("{id}", id.ToString());
+        }
+
+        [FindsBy(How = How.Id, Using = "pageTitle")]
+        public IWebElement PageTitle { get; set; }
+
+        public void VerifyIsCurrentPage()
+        {
+            Wait.Until(ExpectedConditions.TitleIs(Title));
+        }
+
+        public void VerifyIncidentReported()
+        {
+            var by = By.PartialLinkText("Value cannot be null");
+            //var element = WebDriver.FindElement(by);
+            Wait.Until(ExpectedConditions.ElementExists(by));
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Pages/PageHelper.cs b/src/Server/Coderr.Server.Web.Tests/Pages/PageHelper.cs
new file mode 100644
index 00000000..05b8b79a
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Pages/PageHelper.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Text.RegularExpressions;
+using codeRR.Server.Web.Tests.Pages.Account;
+using OpenQA.Selenium;
+
+namespace codeRR.Server.Web.Tests.Pages
+{
+    public class PageHelper
+    {
+        public static IPage ResolvePage(IWebDriver webDriver)
+        {
+            var match = Regex.Match(webDriver.Url, @"/#/$", RegexOptions.IgnoreCase);
+            if (match.Success)
+                return new HomePage(webDriver);
+            match = Regex.Match(webDriver.Url, @"/Account/Login", RegexOptions.IgnoreCase);
+            if (match.Success)
+                return new LoginPage(webDriver);
+            match = Regex.Match(webDriver.Url, @"/#/application/(\d+)", RegexOptions.IgnoreCase);
+            if(match.Success)
+                return new ApplicationPage(webDriver, Convert.ToInt16(match.Groups[1].Value));
+
+            throw new ArgumentOutOfRangeException($"Url: {webDriver.Url}, Title: {webDriver.Title}");
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/AdminUserTests.cs b/src/Server/Coderr.Server.Web.Tests/Tests/AdminUserTests.cs
new file mode 100644
index 00000000..972dffa9
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/AdminUserTests.cs
@@ -0,0 +1,35 @@
+using System;
+using codeRR.Server.Web.Tests.Helpers.Extensions;
+using codeRR.Server.Web.Tests.Pages;
+using OpenQA.Selenium;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    [Trait("Category", "Integration")]
+    public class AdminUserTests : LoggedInTest, IDisposable
+    {
+        private readonly IPage _homePage;
+
+        public AdminUserTests()
+        {
+            _homePage = Login();
+        }
+
+        [Fact]
+        public void Admin_Should_have_create_new_application_menu_item()
+        {
+            UITest(() =>
+            {
+                Assert.IsType(_homePage);
+                Assert.Equal("Overview", WebDriver.WaitUntilTitleEquals(_homePage.Title));
+                Assert.NotNull(WebDriver.FindElement(By.XPath("//a/span[.='Create new application']")));
+            });
+        }
+        
+        public void Dispose()
+        {
+            Logout();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/ConfigureApplicationPageTests.cs b/src/Server/Coderr.Server.Web.Tests/Tests/ConfigureApplicationPageTests.cs
new file mode 100644
index 00000000..e9481110
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/ConfigureApplicationPageTests.cs
@@ -0,0 +1,53 @@
+using System;
+using codeRR.Server.Web.Tests.Pages;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    [Trait("Category", "Integration")]
+    public class ConfigureApplicationPageTests : LoggedInTest, IDisposable
+    {
+        public ConfigureApplicationPageTests()
+        {
+            Login();
+        }
+
+        [Fact]
+        public void Should_not_be_able_to_create_application_without_name_specified()
+        {
+            UITest(() =>
+            {
+                var sut = new ConfigureApplicationPage(WebDriver)
+                    .CreateApplication(string.Empty);
+
+                //TODO: Verify error message
+                sut.VerifyIsCurrentPage();
+            });
+        }
+
+        [Fact]
+        public void Should_be_able_to_create_application()
+        {
+            UITest(() =>
+            {
+                var applicationName = "TestApplication";
+
+                var sut = new ConfigureApplicationPage(WebDriver)
+                    .CreateApplication(applicationName);
+
+                sut.VerifySuccessfullyCreatedApplication(applicationName);
+
+                var homePage = new HomePage(WebDriver);
+                homePage.NavigateToPage();
+                homePage.VerifyIsCurrentPage();
+
+                homePage.HasApplicationInNavigation(applicationName);
+            });
+        }
+
+        public void Dispose()
+        {
+            Logout();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/HomePageTests.cs b/src/Server/Coderr.Server.Web.Tests/Tests/HomePageTests.cs
new file mode 100644
index 00000000..0a3de5f5
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/HomePageTests.cs
@@ -0,0 +1,34 @@
+using System;
+using codeRR.Server.Web.Tests.Pages;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    [Trait("Category", "Integration")]
+    public class HomePageTests : LoggedInTest, IDisposable
+    {
+        public HomePageTests()
+        {
+            Login();
+        }
+
+        [Fact]
+        public void Should_be_able_to_navigate_to_myfirstapp_application()
+        {
+            UITest(() =>
+            {
+                var sut = new HomePage(WebDriver);
+                sut.NavigateToPage();
+
+                sut.NavigationMyTestApp.Click();
+
+                sut.VerifyNavigatedToMyTestApp();
+            });
+        }
+
+        public void Dispose()
+        {
+            Logout();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/IncidentsPageTests.cs b/src/Server/Coderr.Server.Web.Tests/Tests/IncidentsPageTests.cs
new file mode 100644
index 00000000..972ffc9c
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/IncidentsPageTests.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Threading;
+using codeRR.Client;
+using codeRR.Server.Web.Tests.Pages;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    [Trait("Category", "Integration")]
+    public class IncidentsPageTests : LoggedInTest, IDisposable
+    {
+        public IncidentsPageTests()
+        {
+            Login();
+        }
+
+        [Fact]
+        public void Should_be_able_to_report_error_with_client_lib_and_error_shows_up_in_incidents()
+        {
+            UITest(() =>
+            {
+                var url = new Uri(ServerUrl);
+                Err.Configuration.Credentials(url, TestData.Application.AppKey, TestData.Application.SharedSecret);
+                Err.Report(new ArgumentNullException("id"), new { SampleData = "Context example" });
+
+                // Give the server some time to process the incident
+                Thread.Sleep(3000);
+
+                var sut = new IncidentsPage(WebDriver, 1);
+                sut.NavigateToPage();
+
+                sut.VerifyIncidentReported();
+            });
+        }
+
+        public void Dispose()
+        {
+            Logout();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/LoggedInTest.cs b/src/Server/Coderr.Server.Web.Tests/Tests/LoggedInTest.cs
new file mode 100644
index 00000000..45e3b540
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/LoggedInTest.cs
@@ -0,0 +1,32 @@
+using codeRR.Server.Web.Tests.Pages;
+using codeRR.Server.Web.Tests.Pages.Account;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    public class LoggedInTest : WebTest
+    {
+        public IPage Login()
+        {
+            return Login(TestData.TestUser.Username, TestData.TestUser.Password);
+        }
+
+        public IPage Login(string userName, string password)
+        {
+            var page = new LoginPage(WebDriver)
+                .LoginWithValidCredentials(userName, password);
+
+            return page;
+        }
+
+        public LoginPage Logout()
+        {
+            var page = new LogoutPage(WebDriver)
+                .Logout();
+
+            page.VerifyIsCurrentPage();
+
+            return page;
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/LoginPageTests.cs b/src/Server/Coderr.Server.Web.Tests/Tests/LoginPageTests.cs
new file mode 100644
index 00000000..ba0b1bfa
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/LoginPageTests.cs
@@ -0,0 +1,103 @@
+using codeRR.Server.Web.Tests.Pages;
+using codeRR.Server.Web.Tests.Pages.Account;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    [Trait("Category", "Integration")]
+    public class LoginPageTests : LoggedInTest
+    {
+        [Fact]
+        public void Should_not_be_able_to_login_with_empty_username()
+        {
+            UITest(() =>
+            {
+                var sut = new LoginPage(WebDriver)
+                    .LoginWithNoUserNameSpecified();
+
+                Assert.IsType(sut);
+                sut.VerifyIsCurrentPage();
+            });
+        }
+
+        [Fact]
+        public void Should_not_be_able_to_login_with_empty_password()
+        {
+            UITest(() =>
+            {
+                var sut = new LoginPage(WebDriver)
+                    .LoginWithNoPasswordSpecified();
+
+                Assert.IsType(sut);
+                sut.VerifyIsCurrentPage();
+            });
+        }
+
+        [Fact]
+        public void Should_not_be_able_to_login_with_empty_username_and_empty_password()
+        {
+            UITest(() =>
+            {
+                var sut = new LoginPage(WebDriver)
+                    .LoginWithNoUserNameAndNoPasswordSpecified();
+
+                Assert.IsType(sut);
+                sut.VerifyIsCurrentPage();
+            });
+        }
+
+        [Fact]
+        public void Should_not_be_able_to_login_with_non_existing_user()
+        {
+            UITest(() =>
+            {
+                var sut = new LoginPage(WebDriver)
+                    .LoginWithNonExistingUserWithPasswordSpecified();
+
+                Assert.IsType(sut);
+                sut.VerifyIsCurrentPage();
+            });
+        }
+
+        [Fact]
+        public void Should_not_be_able_to_login_with_non_existing_user_with_empty_password()
+        {
+            UITest(() =>
+            {
+                var sut = new LoginPage(WebDriver)
+                    .LoginWithNonExistingUserWithoutPasswordSpecified();
+
+                Assert.IsType(sut);
+                sut.VerifyIsCurrentPage();
+            });
+        }
+
+        [Fact]
+        public void Should_not_be_able_to_login_with_wrong_password()
+        {
+            UITest(() =>
+            {
+                var sut = new LoginPage(WebDriver)
+                    .LoginWithWrongPasswordSpecified();
+
+                Assert.IsType(sut);
+                sut.VerifyIsCurrentPage();
+            });
+        }
+
+        [Fact]
+        public void Should_be_able_to_login_with_valid_credentials()
+        {
+            UITest(() =>
+            {
+                var sut = new LoginPage(WebDriver)
+                    .LoginWithValidCredentials(TestData.TestUser.Username, TestData.TestUser.Password);
+
+                Assert.IsType(sut);
+                ((HomePage)sut).VerifyIsCurrentPage();
+
+                Logout();
+            });
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/NavigationTests.cs b/src/Server/Coderr.Server.Web.Tests/Tests/NavigationTests.cs
new file mode 100644
index 00000000..f5ac112d
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/NavigationTests.cs
@@ -0,0 +1,73 @@
+using System;
+using codeRR.Server.Web.Tests.Helpers.Extensions;
+using codeRR.Server.Web.Tests.Pages;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    [Trait("Category", "Integration")]
+    public class NavigationTests : LoggedInTest, IDisposable
+    {
+        public NavigationTests()
+        {
+            Login();
+        }
+
+        [Fact]
+        public void Should_be_able_to_navigate_to_dashboard()
+        {
+            UITest(() =>
+            {
+                var sut = new HomePage(WebDriver);
+                sut.NavigateToPage();
+                sut.NavigationDashboard.Click();
+
+                Assert.Equal("Overview", WebDriver.WaitUntilTitleEquals("Overview"));
+            });
+        }
+
+        [Fact]
+        public void Should_be_able_to_navigate_to_dashboard_overview()
+        {
+            UITest(() =>
+            {
+                var sut = new HomePage(WebDriver);
+                sut.NavigateToPage();
+                sut.NavigationDashboardOverview.Click();
+
+                Assert.Equal("Overview", WebDriver.WaitUntilTitleEquals("Overview"));
+            });
+        }
+
+        [Fact]
+        public void Should_be_able_to_navigate_to_dashboard_incidents()
+        {
+            UITest(() =>
+            {
+                var sut = new HomePage(WebDriver);
+                sut.NavigateToPage();
+                sut.NavigationDashboardIncidents.Click();
+
+                Assert.Equal("Incidents", WebDriver.WaitUntilTitleEquals("Incidents"));
+            });
+        }
+
+        [Fact]
+        public void Should_be_able_to_navigate_to_dashboard_feedback()
+        {
+            UITest(() =>
+            {
+                var sut = new HomePage(WebDriver);
+                sut.NavigateToPage();
+                sut.NavigationDashboardFeedback.Click();
+
+                Assert.Equal("All feedback", WebDriver.WaitUntilTitleEquals("All feedback"));
+            });
+        }
+
+        public void Dispose()
+        {
+            Logout();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/RegisterPageTests.cs b/src/Server/Coderr.Server.Web.Tests/Tests/RegisterPageTests.cs
new file mode 100644
index 00000000..db142f1e
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/RegisterPageTests.cs
@@ -0,0 +1,52 @@
+using codeRR.Server.Web.Tests.Helpers.Extensions;
+using codeRR.Server.Web.Tests.Pages.Account;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    [Trait("Category", "Integration")]
+    public class RegisterPageTests : LoggedInTest
+    {
+        [Fact]
+        public void Should_not_be_able_to_register_using_already_taken_username()
+        {
+            UITest(() =>
+            {
+                var sut = new RegisterPage(WebDriver)
+                    .RegisterUsingAlreadyTakenUsername();
+
+                Assert.IsType(sut);
+                sut.VerifyIsCurrentPage();
+
+                sut.VerifyUsernameIsAlreadyTaken();
+            });
+        }
+
+        [Fact]
+        public void Should_not_be_able_to_register_using_already_taken_email()
+        {
+            UITest(() =>
+            {
+                var sut = new RegisterPage(WebDriver)
+                    .RegisterUsingAlreadyTakenEmail();
+
+                Assert.IsType(sut);
+                sut.VerifyIsCurrentPage();
+
+                sut.VerifyEmailIsAlreadyTaken();
+            });
+        }
+
+        [Fact]
+        public void Should_be_able_to_register_user()
+        {
+            UITest(() =>
+            {
+                var sut = new RegisterPage(WebDriver)
+                    .RegisterNewUser();
+
+                Assert.Equal("Account registered - codeRR", WebDriver.WaitUntilTitleEquals(sut.Title));
+            });
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/Tests/UserTests.cs b/src/Server/Coderr.Server.Web.Tests/Tests/UserTests.cs
new file mode 100644
index 00000000..a06fe7a6
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/Tests/UserTests.cs
@@ -0,0 +1,50 @@
+using System;
+using codeRR.Server.SqlServer.Tests.Models;
+using codeRR.Server.Web.Tests.Helpers.Extensions;
+using codeRR.Server.Web.Tests.Pages;
+using OpenQA.Selenium;
+using Xunit;
+
+namespace codeRR.Server.Web.Tests.Tests
+{
+    [Trait("Category", "Integration")]
+    public class UserTests : LoggedInTest, IDisposable
+    {
+        private readonly IPage _homePage;
+        private readonly TestUser _testUser;
+
+        public UserTests()
+        {
+            _testUser = new TestUser { Username = "TestNormalUser", Password = "123456", Email = "TestNormalUser@coderrapp.com" };
+            TestData.CreateUser(_testUser, TestData.ApplicationId);
+
+            _homePage = Login(_testUser.Username, _testUser.Password);
+        }
+
+        [Fact]
+        public void NormalUser_Should_not_have_create_new_application_menu_item()
+        {
+            UITest(() =>
+            {
+                Assert.IsType(_homePage);
+                Assert.Equal("MyTestApp", WebDriver.WaitUntilTitleEquals("MyTestApp"));
+                Assert.Throws(() => WebDriver.FindElement(By.XPath("//a/span[.='Create new application']")));
+            });
+        }
+
+        [Fact]
+        public void NormalUser_Should_see_homepage_after_login()
+        {
+            UITest(() =>
+            {
+                Assert.IsType(_homePage);
+                Assert.Equal("MyTestApp", WebDriver.WaitUntilTitleEquals("MyTestApp"));
+            });
+        }
+
+        public void Dispose()
+        {
+            Logout();
+        }
+    }
+}
diff --git a/src/Server/Coderr.Server.Web.Tests/WebTest.cs b/src/Server/Coderr.Server.Web.Tests/WebTest.cs
new file mode 100644
index 00000000..bca6fe48
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/WebTest.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Web.Configuration;
+using codeRR.Server.SqlServer.Core.Accounts;
+using codeRR.Server.SqlServer.Tests.Helpers;
+using codeRR.Server.SqlServer.Tests.Models;
+using codeRR.Server.Web.Tests.Helpers;
+using codeRR.Server.Web.Tests.Helpers.Selenium;
+using Griffin.Data.Mapper;
+using OpenQA.Selenium;
+using OpenQA.Selenium.Support.Extensions;
+using Xunit;
+
+[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly)]
+
+namespace codeRR.Server.Web.Tests
+{
+    [TestCaseOrderer("codeRR.Server.Web.Tests.Helpers.xUnit.TestCaseOrderer", "codeRR.Server.Web.Tests")]
+    public abstract class WebTest
+    {
+        private static readonly IisExpressHelper _iisExpress;
+        private static readonly DatabaseManager _databaseManager = new DatabaseManager();
+
+        static WebTest()
+        {
+            var mapper = new AssemblyScanningMappingProvider();
+            mapper.Scan(typeof(AccountRepository).Assembly);
+            EntityMappingProvider.Provider = mapper;
+
+            _databaseManager.CreateEmptyDatabase();
+            _databaseManager.InitSchema();
+
+            AppDomain.CurrentDomain.DomainUnload += (o, e) =>
+            {
+                _iisExpress?.Stop();
+                _databaseManager.Dispose();
+            };
+
+            // Disables database migration in codeRR.Server.Web project, should be up-to-date already
+            // SchemaUpdateModule does not handle coderr_ConnectionString environment variable
+            // This should only be run on build server due to changes in web.config
+            if (Environment.GetEnvironmentVariable("TF_BUILD") != null)
+            {
+                DisableDatabaseMigrations();
+            }
+
+            var configPath =
+                Path.Combine(Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\..\")),
+                    "applicationhost.config");
+
+            Console.WriteLine($"Path to IIS Express configuration file '{configPath}'");
+
+            _iisExpress = new IisExpressHelper
+            {
+                ConfigPath = configPath,
+
+                // Pass on connectionstring to codeRR.Server.Web during testing, overriding connectionstring in web.config
+                EnvironmentVariables = new Dictionary { { "coderr_ConnectionString", _databaseManager.ConnectionString } }
+            };
+            _iisExpress.Start("codeRR.Server.Web");
+
+            // Warmup request only on build server
+            if (Environment.GetEnvironmentVariable("TF_BUILD") != null)
+            {
+                var webClient = new WebClient();
+                webClient.DownloadString(_iisExpress.BaseUrl);
+            }
+
+            TestUser = new TestUser {Username = "TestUser", Password = "123456", Email = "TestUser@coderrapp.com"};
+
+            TestData = new TestDataManager(_databaseManager.OpenConnection) { TestUser = TestUser };
+
+            WebDriver = DriverFactory.Create(BrowserType.Chrome);
+            AppDomain.CurrentDomain.DomainUnload += (o, e) => { DisposeWebDriver(); };
+        }
+
+        /// 
+        /// Disables database migration in codeRR.Server.Web project
+        /// 
+        private static void DisableDatabaseMigrations()
+        {
+            var webConfigFilename =
+                Path.Combine(
+                    Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"..\..\..\..\codeRR.Server.Web\")),
+                    "web.config");
+
+            Console.WriteLine($"Setting DisableMigrations=true in '{webConfigFilename}'");
+
+            // Prevent SchemaUpdateModule from running
+            var configFile = new FileInfo(webConfigFilename);
+            var vdm = new VirtualDirectoryMapping(configFile.DirectoryName, true, configFile.Name);
+            var wcfm = new WebConfigurationFileMap();
+            wcfm.VirtualDirectories.Add("/", vdm);
+            var configuration = WebConfigurationManager.OpenMappedWebConfiguration(wcfm, "/");
+            var appSettings = configuration.AppSettings.Settings;
+            if (appSettings["DisableMigrations"] == null)
+                appSettings.Add("DisableMigrations", "true");
+            else
+                appSettings["DisableMigrations"].Value = "true";
+            configuration.Save();
+        }
+
+        protected WebTest()
+        {
+            TestData.ResetDatabase(_iisExpress.BaseUrl);
+        }
+
+        public string ServerUrl => _iisExpress.BaseUrl;
+
+        public static TestDataManager TestData { get; }
+
+        public static IWebDriver WebDriver { get; private set; }
+
+        public static TestUser TestUser { get; set; }
+
+        private static void DisposeWebDriver()
+        {
+            try
+            {
+                WebDriver.Quit();
+            }
+            // ReSharper disable once EmptyGeneralCatchClause
+            catch
+            {
+            }
+
+            WebDriver.Dispose();
+        }
+
+        // ReSharper disable once InconsistentNaming
+        public void UITest(Action action)
+        {
+            try
+            {
+                action();
+            }
+            catch
+            {
+                var screenshot = WebDriver.TakeScreenshot();
+
+                var fileName = Path.Combine(AppDomain.CurrentDomain.BaseDirectory,
+                    $"{GetType().Name}.{DateTime.Now:yyyMMdd.HHmmss}.png");
+
+                screenshot.SaveAsFile(fileName, ScreenshotImageFormat.Png);
+
+                throw;
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/Server/Coderr.Server.Web.Tests/applicationhost.config b/src/Server/Coderr.Server.Web.Tests/applicationhost.config
new file mode 100644
index 00000000..161775d3
--- /dev/null
+++ b/src/Server/Coderr.Server.Web.Tests/applicationhost.config
@@ -0,0 +1,1048 @@
+
+
+
+
+
+
+
+
+
+
+    
+    
+        
+            
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Server/Coderr.Server.Web.Tests/applicationhost.tt b/src/Server/Coderr.Server.Web.Tests/applicationhost.tt new file mode 100644 index 00000000..1c28866c --- /dev/null +++ b/src/Server/Coderr.Server.Web.Tests/applicationhost.tt @@ -0,0 +1,1048 @@ +<#@ template debug="true" hostspecific="true" language="C#" #> +<#@ assembly name="System.Core" #> +<#@ import namespace="System.Linq" #> +<#@ import namespace="System.Text" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ output extension=".config" #> + + + + + + + +
+
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
+ + +
+
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + " /> + + + " /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Server/Coderr.Server.WebPush/Coderr.Server.WebPush.csproj b/src/Server/Coderr.Server.WebPush/Coderr.Server.WebPush.csproj new file mode 100644 index 00000000..90b84a0a --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Coderr.Server.WebPush.csproj @@ -0,0 +1,19 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + diff --git a/src/Server/Coderr.Server.WebPush/Model/NotificationAction.cs b/src/Server/Coderr.Server.WebPush/Model/NotificationAction.cs new file mode 100644 index 00000000..e8c78cf2 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Model/NotificationAction.cs @@ -0,0 +1,29 @@ +using System; +using Newtonsoft.Json; + +namespace Coderr.Server.WebPush.Model +{ + /// + /// Notification API Standard + /// + public class NotificationAction + { + public NotificationAction(string action, string title) + { + Action = action ?? throw new ArgumentNullException(nameof(action)); + Title = title ?? throw new ArgumentNullException(nameof(title)); + } + + /// + /// Action in the service worker. + /// + [JsonProperty("action")] + public string Action { get; private set; } + + /// + /// Title shown for the action. + /// + [JsonProperty("title")] + public string Title { get; private set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/Model/PushNotification.cs b/src/Server/Coderr.Server.WebPush/Model/PushNotification.cs new file mode 100644 index 00000000..2a3df390 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Model/PushNotification.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Coderr.Server.WebPush.Model +{ + /// + /// Notification API Standard + /// + public class PushNotification + { + public PushNotification() + { + } + + public PushNotification(string text) + { + Body = text; + } + + [JsonProperty("actions")] + public List Actions { get; set; } = + new List(); + + [JsonProperty("data", TypeNameHandling = TypeNameHandling.None)] + public object Data { get; set; } + + [JsonProperty("badge")] public string Badge { get; set; } + + [JsonProperty("body")] public string Body { get; set; } + + [JsonProperty("icon")] public string Icon { get; set; } + + [JsonProperty("image")] public string Image { get; set; } + + [JsonProperty("lang")] public string Lang { get; set; } = "en"; + + [JsonProperty("requireInteraction")] public bool RequireInteraction { get; set; } + + [JsonProperty("tag")] public string Tag { get; set; } + + [JsonProperty("timestamp")] public DateTime Timestamp { get; set; } = DateTime.Now; + + [JsonProperty("title")] public string Title { get; set; } = "Push Demo"; + } +} diff --git a/src/Server/Coderr.Server.WebPush/Model/PushSubscription.cs b/src/Server/Coderr.Server.WebPush/Model/PushSubscription.cs new file mode 100644 index 00000000..e8b5b361 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Model/PushSubscription.cs @@ -0,0 +1,22 @@ +using System; + +namespace Coderr.Server.WebPush.Model +{ + public class PushSubscription + { + protected PushSubscription() + { + } + + public PushSubscription(string endpoint, string p256dh, string auth) + { + Endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); + P256DH = p256dh ?? throw new ArgumentNullException(nameof(p256dh)); + Auth = auth; + } + + public string Endpoint { get; set; } + public string P256DH { get; set; } + public string Auth { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/Model/VapidDetails.cs b/src/Server/Coderr.Server.WebPush/Model/VapidDetails.cs new file mode 100644 index 00000000..1e6c5275 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Model/VapidDetails.cs @@ -0,0 +1,102 @@ +using System; +using Coderr.Server.WebPush.Util; + +namespace Coderr.Server.WebPush.Model +{ + public class VapidDetails + { + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0); + private long _expiration = -1; + private string _privateKey; + private string _publicKey; + private string _subject; + public const string LiveSubject = "mailto:help@coderr.io"; + + protected VapidDetails() + { + } + + /// This should be a URL or a 'mailto:' email address + /// The VAPID public key as a base64 encoded string + /// The VAPID private key as a base64 encoded string + public VapidDetails(string subject, string publicKey, string privateKey) + { + Subject = subject; + PublicKey = publicKey; + PrivateKey = privateKey; + } + + /// + /// Unix epoch based expiration. Default is 12 hours from now. + /// + public long Expiration + { + get => _expiration == -1 ? UnixTimeNow + 43200 : _expiration; + set + { + if (value != -1 && value <= UnixTimeNow) + throw new ArgumentException(@"Vapid expiration must be a unix timestamp in the future"); + _expiration = value; + } + } + + /// + /// Must be a 32 bytes long key encoded as Base64 + /// + public string PrivateKey + { + get => _privateKey; + set + { + if (string.IsNullOrEmpty(value)) throw new ArgumentException(@"Valid private key not set"); + + var decodedPrivateKey = UrlBase64Helper.Decode(value); + + if (decodedPrivateKey.Length != 32) + throw new ArgumentException(@"Vapid private key should be 32 bytes long when decoded."); + _privateKey = value; + } + } + + /// + /// Must be a 65 bytes long key encoded as Base64 + /// + public string PublicKey + { + get => _publicKey; + set + { + if (string.IsNullOrEmpty(value)) throw new ArgumentException(@"Valid public key not set"); + + var decodedPublicKey = UrlBase64Helper.Decode(value); + + if (decodedPublicKey.Length != 65) + throw new ArgumentException(@"Vapid public key must be 65 characters long when decoded"); + _publicKey = value; + } + } + + /// + /// This should be a URL or a 'mailto:' email address. + /// + public string Subject + { + get => _subject; + set + { + if (string.IsNullOrEmpty(value)) throw new ArgumentException(@"A subject is required"); + + if (value.Length == 0) + throw new ArgumentException( + @"The subject value must be a string containing a url or mailto: address."); + + if (!value.StartsWith("mailto:") && !Uri.IsWellFormedUriString(value, UriKind.Absolute)) + throw new ArgumentException(@"Subject is not a valid URL or mailto address"); + + _subject = value; + } + } + + private static long UnixTimeNow => (long)DateTime.UtcNow.Subtract(UnixEpoch).TotalSeconds; + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/ReadMe.md b/src/Server/Coderr.Server.WebPush/ReadMe.md new file mode 100644 index 00000000..40f00ea8 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/ReadMe.md @@ -0,0 +1,8 @@ +Push notifications for .NET +============================ + +This is really the code from https://github.com/web-push-libs/web-push-csharp. +But that library was not readable nor easy to use. + +So I've basically refactored and cleaned up the code. + diff --git a/src/Server/Coderr.Server.WebPush/Util/ECKeyHelper.cs b/src/Server/Coderr.Server.WebPush/Util/ECKeyHelper.cs new file mode 100644 index 00000000..3bf5e138 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Util/ECKeyHelper.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using Org.BouncyCastle.Asn1; +using Org.BouncyCastle.Asn1.Nist; +using Org.BouncyCastle.Crypto; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.OpenSsl; +using Org.BouncyCastle.Security; + +namespace Coderr.Server.WebPush.Util +{ + internal static class ECKeyHelper + { + public static ECPrivateKeyParameters GetPrivateKey(byte[] privateKey) + { + Asn1Object version = new DerInteger(1); + Asn1Object derEncodedKey = new DerOctetString(privateKey); + Asn1Object keyTypeParameters = new DerTaggedObject(0, new DerObjectIdentifier(@"1.2.840.10045.3.1.7")); + + Asn1Object derSequence = new DerSequence(version, derEncodedKey, keyTypeParameters); + + var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); + var pemKey = "-----BEGIN EC PRIVATE KEY-----\n"; + pemKey += base64EncodedDerSequence; + pemKey += "\n-----END EC PRIVATE KEY----"; + + var reader = new StringReader(pemKey); + var pemReader = new PemReader(reader); + var keyPair = (AsymmetricCipherKeyPair)pemReader.ReadObject(); + + return (ECPrivateKeyParameters)keyPair.Private; + } + + public static ECPublicKeyParameters GetPublicKey(byte[] publicKey) + { + Asn1Object keyTypeParameters = new DerSequence(new DerObjectIdentifier(@"1.2.840.10045.2.1"), + new DerObjectIdentifier(@"1.2.840.10045.3.1.7")); + Asn1Object derEncodedKey = new DerBitString(publicKey); + + Asn1Object derSequence = new DerSequence(keyTypeParameters, derEncodedKey); + + var base64EncodedDerSequence = Convert.ToBase64String(derSequence.GetDerEncoded()); + var pemKey = "-----BEGIN PUBLIC KEY-----\n"; + pemKey += base64EncodedDerSequence; + pemKey += "\n-----END PUBLIC KEY-----"; + + var reader = new StringReader(pemKey); + var pemReader = new PemReader(reader); + var keyPair = pemReader.ReadObject(); + return (ECPublicKeyParameters)keyPair; + } + + public static AsymmetricCipherKeyPair GenerateKeys() + { + var ecParameters = NistNamedCurves.GetByName("P-256"); + var ecSpec = new ECDomainParameters(ecParameters.Curve, ecParameters.G, ecParameters.N, ecParameters.H, + ecParameters.GetSeed()); + var keyPairGenerator = GeneratorUtilities.GetKeyPairGenerator("ECDH"); + keyPairGenerator.Init(new ECKeyGenerationParameters(ecSpec, new SecureRandom())); + + return keyPairGenerator.GenerateKeyPair(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/Util/EncryptionResult.cs b/src/Server/Coderr.Server.WebPush/Util/EncryptionResult.cs new file mode 100644 index 00000000..e6d4b622 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Util/EncryptionResult.cs @@ -0,0 +1,20 @@ + +namespace Coderr.Server.WebPush.Util +{ + public class EncryptionResult + { + public byte[] PublicKey { get; set; } + public byte[] Payload { get; set; } + public byte[] Salt { get; set; } + + public string Base64EncodePublicKey() + { + return UrlBase64Helper.Encode(PublicKey); + } + + public string Base64EncodeSalt() + { + return UrlBase64Helper.Encode(Salt); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/Util/Encryptor.cs b/src/Server/Coderr.Server.WebPush/Util/Encryptor.cs new file mode 100644 index 00000000..e6fb1508 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Util/Encryptor.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Org.BouncyCastle.Crypto.Engines; +using Org.BouncyCastle.Crypto.Modes; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Security; + +namespace Coderr.Server.WebPush.Util +{ + // @LogicSoftware + // Originally from https://github.com/LogicSoftware/WebPushEncryption/blob/master/src/Encryptor.cs + internal static class Encryptor + { + public static EncryptionResult Encrypt(string userKey, string userSecret, string payload) + { + var userKeyBytes = UrlBase64Helper.Decode(userKey); + var userSecretBytes = UrlBase64Helper.Decode(userSecret); + var payloadBytes = Encoding.UTF8.GetBytes(payload); + + return Encrypt(userKeyBytes, userSecretBytes, payloadBytes); + } + + public static EncryptionResult Encrypt(byte[] userKey, byte[] userSecret, byte[] payload) + { + var salt = GenerateSalt(16); + var serverKeyPair = ECKeyHelper.GenerateKeys(); + + var ecdhAgreement = AgreementUtilities.GetBasicAgreement("ECDH"); + ecdhAgreement.Init(serverKeyPair.Private); + + var userPublicKey = ECKeyHelper.GetPublicKey(userKey); + + var key = ecdhAgreement.CalculateAgreement(userPublicKey).ToByteArrayUnsigned(); + var serverPublicKey = ((ECPublicKeyParameters)serverKeyPair.Public).Q.GetEncoded(false); + + var prk = HKDF(userSecret, key, Encoding.UTF8.GetBytes("Content-Encoding: auth\0"), 32); + var cek = HKDF(salt, prk, CreateInfoChunk("aesgcm", userKey, serverPublicKey), 16); + var nonce = HKDF(salt, prk, CreateInfoChunk("nonce", userKey, serverPublicKey), 12); + + var input = AddPaddingToInput(payload); + var encryptedMessage = EncryptAes(nonce, cek, input); + + return new EncryptionResult + { + Salt = salt, + Payload = encryptedMessage, + PublicKey = serverPublicKey + }; + } + + private static byte[] GenerateSalt(int length) + { + var salt = new byte[length]; + var random = new Random(); + random.NextBytes(salt); + return salt; + } + + private static byte[] AddPaddingToInput(byte[] data) + { + var input = new byte[0 + 2 + data.Length]; + Buffer.BlockCopy(ConvertInt(0), 0, input, 0, 2); + Buffer.BlockCopy(data, 0, input, 0 + 2, data.Length); + return input; + } + + private static byte[] EncryptAes(byte[] nonce, byte[] cek, byte[] message) + { + var cipher = new GcmBlockCipher(new AesEngine()); + var parameters = new AeadParameters(new KeyParameter(cek), 128, nonce); + cipher.Init(true, parameters); + + //Generate Cipher Text With Auth Tag + var cipherText = new byte[cipher.GetOutputSize(message.Length)]; + var len = cipher.ProcessBytes(message, 0, message.Length, cipherText, 0); + cipher.DoFinal(cipherText, len); + + //byte[] tag = cipher.GetMac(); + return cipherText; + } + + public static byte[] HKDFSecondStep(byte[] key, byte[] info, int length) + { + var hmac = new HmacSha256(key); + var infoAndOne = info.Concat(new byte[] { 0x01 }).ToArray(); + var result = hmac.ComputeHash(infoAndOne); + + if (result.Length > length) + { + Array.Resize(ref result, length); + } + + return result; + } + + public static byte[] HKDF(byte[] salt, byte[] prk, byte[] info, int length) + { + var hmac = new HmacSha256(salt); + var key = hmac.ComputeHash(prk); + + return HKDFSecondStep(key, info, length); + } + + public static byte[] ConvertInt(int number) + { + var output = BitConverter.GetBytes(Convert.ToUInt16(number)); + if (BitConverter.IsLittleEndian) + { + Array.Reverse(output); + } + + return output; + } + + public static byte[] CreateInfoChunk(string type, byte[] recipientPublicKey, byte[] senderPublicKey) + { + var output = new List(); + output.AddRange(Encoding.UTF8.GetBytes($"Content-Encoding: {type}\0P-256\0")); + output.AddRange(ConvertInt(recipientPublicKey.Length)); + output.AddRange(recipientPublicKey); + output.AddRange(ConvertInt(senderPublicKey.Length)); + output.AddRange(senderPublicKey); + return output.ToArray(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/Util/HmacSha256.cs b/src/Server/Coderr.Server.WebPush/Util/HmacSha256.cs new file mode 100644 index 00000000..43a0c159 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Util/HmacSha256.cs @@ -0,0 +1,26 @@ +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Macs; +using Org.BouncyCastle.Crypto.Parameters; + +namespace Coderr.Server.WebPush.Util +{ + public class HmacSha256 + { + private readonly HMac _hmac; + + public HmacSha256(byte[] key) + { + _hmac = new HMac(new Sha256Digest()); + _hmac.Init(new KeyParameter(key)); + } + + public byte[] ComputeHash(byte[] value) + { + var resBuf = new byte[_hmac.GetMacSize()]; + _hmac.BlockUpdate(value, 0, value.Length); + _hmac.DoFinal(resBuf, 0); + + return resBuf; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/Util/JwsSigner.cs b/src/Server/Coderr.Server.WebPush/Util/JwsSigner.cs new file mode 100644 index 00000000..14e926f8 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Util/JwsSigner.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json; +using Org.BouncyCastle.Crypto.Digests; +using Org.BouncyCastle.Crypto.Parameters; +using Org.BouncyCastle.Crypto.Signers; + +namespace Coderr.Server.WebPush.Util +{ + internal class JwsSigner + { + private readonly ECPrivateKeyParameters _privateKey; + + public JwsSigner(ECPrivateKeyParameters privateKey) + { + _privateKey = privateKey; + } + + /// + /// Generates a Jws Signature. + /// + /// + /// + /// + public string GenerateSignature(Dictionary header, Dictionary payload) + { + var securedInput = SecureInput(header, payload); + var message = Encoding.UTF8.GetBytes(securedInput); + + var hashedMessage = Sha256Hash(message); + + var signer = new ECDsaSigner(); + signer.Init(true, _privateKey); + var results = signer.GenerateSignature(hashedMessage); + + // Concated to create signature + var a = results[0].ToByteArrayUnsigned(); + var b = results[1].ToByteArrayUnsigned(); + + // a,b are required to be exactly the same length of bytes + if (a.Length != b.Length) + { + var largestLength = Math.Max(a.Length, b.Length); + a = ByteArrayPadLeft(a, largestLength); + b = ByteArrayPadLeft(b, largestLength); + } + + var signature = UrlBase64Helper.Encode(a.Concat(b).ToArray()); + return $"{securedInput}.{signature}"; + } + + private static byte[] ByteArrayPadLeft(byte[] src, int size) + { + var dst = new byte[size]; + var startAt = dst.Length - src.Length; + Array.Copy(src, 0, dst, startAt, src.Length); + return dst; + } + + private static string SecureInput(Dictionary header, Dictionary payload) + { + var encodeHeader = UrlBase64Helper.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(header))); + var encodePayload = UrlBase64Helper.Encode(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload))); + + return $"{encodeHeader}.{encodePayload}"; + } + + private static byte[] Sha256Hash(byte[] message) + { + var sha256Digest = new Sha256Digest(); + sha256Digest.BlockUpdate(message, 0, message.Length); + var hash = new byte[sha256Digest.GetDigestSize()]; + sha256Digest.DoFinal(hash, 0); + return hash; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/Util/UrlBase64Helper.cs b/src/Server/Coderr.Server.WebPush/Util/UrlBase64Helper.cs new file mode 100644 index 00000000..21a230ad --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Util/UrlBase64Helper.cs @@ -0,0 +1,34 @@ +using System; + +namespace Coderr.Server.WebPush.Util +{ + internal static class UrlBase64Helper + { + /// + /// Decodes a url-safe base64 string into bytes + /// + /// + /// + public static byte[] Decode(string base64) + { + base64 = base64.Replace('-', '+').Replace('_', '/'); + + while (base64.Length % 4 != 0) + { + base64 += "="; + } + + return Convert.FromBase64String(base64); + } + + /// + /// Encodes bytes into url-safe base64 string + /// + /// + /// + public static string Encode(byte[] data) + { + return Convert.ToBase64String(data).Replace('+', '-').Replace('/', '_').TrimEnd('='); + } + } +} diff --git a/src/Server/Coderr.Server.WebPush/Util/VapidHelper.cs b/src/Server/Coderr.Server.WebPush/Util/VapidHelper.cs new file mode 100644 index 00000000..c4d1137f --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Util/VapidHelper.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.WebPush.Model; +using Org.BouncyCastle.Crypto.Parameters; + +namespace Coderr.Server.WebPush.Util +{ + public static class VapidHelper + { + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0); + + /// + /// Generate vapid keys + /// + public static VapidDetails GenerateVapidKeys(string subject) + { + var keys = ECKeyHelper.GenerateKeys(); + var publicKey = ((ECPublicKeyParameters)keys.Public).Q.GetEncoded(false); + var privateKey = ((ECPrivateKeyParameters)keys.Private).D.ToByteArrayUnsigned(); + + + var publicKeyBase64 = UrlBase64Helper.Encode(publicKey); + var privateKeyBase64 = UrlBase64Helper.Encode(ByteArrayPadLeft(privateKey, 32)); + + return new VapidDetails(subject, publicKeyBase64, privateKeyBase64); + } + + /// + /// This method takes the required VAPID parameters and returns the required + /// header to be added to a Web Push Protocol Request. + /// + /// This must be the origin of the push service. + /// + /// A dictionary of header key/value pairs. + public static VapidHttpHeaders GetHttpHeaders(string audience, VapidDetails details) + { + if (details == null) throw new ArgumentNullException(nameof(details)); + ValidateAudience(audience); + + var decodedPrivateKey = UrlBase64Helper.Decode(details.PrivateKey); + + + var header = new Dictionary {{"typ", "JWT"}, {"alg", "ES256"}}; + + var jwtPayload = new Dictionary + { + {"aud", audience}, + {"exp", details.Expiration}, + {"sub", details.Subject} + }; + + var signingKey = ECKeyHelper.GetPrivateKey(decodedPrivateKey); + + var signer = new JwsSigner(signingKey); + var token = signer.GenerateSignature(header, jwtPayload); + + return new VapidHttpHeaders + { + CryptoKey = "p256ecdsa=" + details.PublicKey, + Authorization = "WebPush " + token + }; + } + + public static void ValidateAudience(string audience) + { + if (string.IsNullOrEmpty(audience)) + throw new ArgumentException(@"No audience could be generated for VAPID."); + + if (audience.Length == 0) + throw new ArgumentException( + @"The audience value must be a string containing the origin of a push service. " + audience); + + if (!Uri.IsWellFormedUriString(audience, UriKind.Absolute)) + throw new ArgumentException(@"VAPID audience is not a url."); + } + + + private static byte[] ByteArrayPadLeft(byte[] src, int size) + { + var dst = new byte[size]; + var startAt = dst.Length - src.Length; + Array.Copy(src, 0, dst, startAt, src.Length); + return dst; + } + + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/Util/VapidHttpHeaders.cs b/src/Server/Coderr.Server.WebPush/Util/VapidHttpHeaders.cs new file mode 100644 index 00000000..082b8cbd --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/Util/VapidHttpHeaders.cs @@ -0,0 +1,8 @@ +namespace Coderr.Server.WebPush.Util +{ + public class VapidHttpHeaders + { + public string CryptoKey { get; set; } + public string Authorization { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/WebPushClient.cs b/src/Server/Coderr.Server.WebPush/WebPushClient.cs new file mode 100644 index 00000000..d129aaa6 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/WebPushClient.cs @@ -0,0 +1,142 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Coderr.Server.WebPush.Model; +using Coderr.Server.WebPush.Util; +using Newtonsoft.Json; + +namespace Coderr.Server.WebPush +{ + public class WebPushClient : IDisposable + { + // default TTL is 4 weeks. + private const int DefaultTtl = 2419200; + + private string _gcmApiKey; + private HttpClient _httpClient = new HttpClient(); + private VapidDetails _vapidDetails; + + public WebPushClient(VapidDetails vapidDetails) + { + _vapidDetails = vapidDetails ?? throw new ArgumentNullException(nameof(vapidDetails)); + } + + public WebPushClient(string gcmApiKey) + { + if (gcmApiKey == null) + { + _gcmApiKey = null; + return; + } + + if (string.IsNullOrEmpty(gcmApiKey)) + { + throw new ArgumentException(@"The GCM API Key should be a non-empty string or null."); + } + + _gcmApiKey = gcmApiKey; + } + + + /// + /// To get a request without sending a push notification call this method. + /// This method will throw an ArgumentException if there is an issue with the input. + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + /// A HttpRequestMessage object that can be sent. + protected HttpRequestMessage GenerateRequestDetails(PushSubscription subscription, string payload) + { + if (subscription == null) throw new ArgumentNullException(nameof(subscription)); + if (payload == null) throw new ArgumentNullException(nameof(payload)); + if (!Uri.IsWellFormedUriString(subscription.Endpoint, UriKind.Absolute)) + { + throw new ArgumentException(@"You must pass in a subscription with at least a valid endpoint"); + } + + var request = new HttpRequestMessage(HttpMethod.Post, subscription.Endpoint); + var currentGcmApiKey = _gcmApiKey; + + request.Headers.Add("TTL", DefaultTtl.ToString()); + var encryptedPayload = Encryptor.Encrypt(subscription.P256DH, subscription.Auth, payload); + request.Content = new ByteArrayContent(encryptedPayload.Payload); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + request.Content.Headers.ContentLength = encryptedPayload.Payload.Length; + request.Content.Headers.ContentEncoding.Add("aesgcm"); + request.Headers.Add("Encryption", "salt=" + encryptedPayload.Base64EncodeSalt()); + var cryptoKeyHeader = @"dh=" + encryptedPayload.Base64EncodePublicKey(); + + + var isGcm = subscription.Endpoint.StartsWith(@"https://android.googleapis.com/gcm/send"); + if (isGcm) + { + if (!string.IsNullOrEmpty(currentGcmApiKey)) + { + request.Headers.TryAddWithoutValidation("Authorization", $"key={currentGcmApiKey}"); + } + } + else if (_vapidDetails != null) + { + var uri = new Uri(subscription.Endpoint); + var audience = $@"{uri.Scheme}://{uri.Host}"; + + var vapidHeaders = VapidHelper.GetHttpHeaders(audience, _vapidDetails); + request.Headers.Add(@"Authorization", vapidHeaders.Authorization); + if (string.IsNullOrEmpty(cryptoKeyHeader)) + { + cryptoKeyHeader = vapidHeaders.CryptoKey; + } + else + { + cryptoKeyHeader += @";" + vapidHeaders.CryptoKey; + } + } + + request.Headers.Add("Crypto-Key", cryptoKeyHeader); + return request; + } + + /// + /// To send a push notification call this method with a subscription, optional payload and any options + /// Will exception if unsuccessful + /// + /// The PushSubscription you wish to send the notification to. + /// The payload you wish to send to the user + public async Task NotifyAsync(PushSubscription subscription, PushNotification notification) + { + var json = JsonConvert.SerializeObject(notification); + var request = GenerateRequestDetails(subscription, json); + var response = await _httpClient.SendAsync(request); + await HandleResponse(response, subscription); + } + + /// + /// Handle Web Push responses. + /// + /// + /// + private static async Task HandleResponse(HttpResponseMessage response, PushSubscription subscription) + { + // Successful + if (response.StatusCode == HttpStatusCode.Created || + response.StatusCode == HttpStatusCode.Accepted) + { + return; + } + + var msg = await response.Content.ReadAsStringAsync(); + throw new WebPushException($"{response.StatusCode}: {msg}", response.StatusCode, response.Headers, subscription); + } + + public void Dispose() + { + if (_httpClient == null) + return; + + _httpClient.Dispose(); + _httpClient = null; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebPush/WebPushException.cs b/src/Server/Coderr.Server.WebPush/WebPushException.cs new file mode 100644 index 00000000..cc65d5b5 --- /dev/null +++ b/src/Server/Coderr.Server.WebPush/WebPushException.cs @@ -0,0 +1,22 @@ +using System; +using System.Net; +using System.Net.Http.Headers; +using Coderr.Server.WebPush.Model; + +namespace Coderr.Server.WebPush +{ + public class WebPushException : Exception + { + public WebPushException(string message, HttpStatusCode statusCode, HttpResponseHeaders headers, + PushSubscription pushSubscription) : base(message) + { + StatusCode = statusCode; + Headers = headers; + PushSubscription = pushSubscription; + } + + public HttpStatusCode StatusCode { get; set; } + public HttpResponseHeaders Headers { get; set; } + public PushSubscription PushSubscription { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/.gitignore b/src/Server/Coderr.Server.WebSite/.gitignore new file mode 100644 index 00000000..41ffa34d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/.gitignore @@ -0,0 +1,231 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +bin/ +Bin/ +obj/ +Obj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +orleans.codegen.cs + +/node_modules + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/AccountController.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/AccountController.cs new file mode 100644 index 00000000..d5dedc25 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/AccountController.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Domain.Core.Account; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Domain.Core.User; +using Coderr.Server.Infrastructure; +using Coderr.Server.SqlServer.Core.Accounts; +using Coderr.Server.SqlServer.Core.Applications; +using Coderr.Server.SqlServer.Core.Users; +using Coderr.Server.WebSite.Areas.Installation.Models; +using Griffin.Data; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Coderr.Server.WebSite.Areas.Installation.Controllers +{ + [Area("Installation")] + + public class AccountController : Controller + { + public ActionResult Admin() + { + SetStateFlag(); + var model = new AccountViewModel(); + return View(model); + } + + + [HttpPost] + public async Task Admin(AccountViewModel model) + { + SetStateFlag(); + + if (!ModelState.IsValid) + return View(model); + + try + { + var account = new Account(model.UserName, model.Password); + account.Activate(); + account.IsSysAdmin = true; + var con = SetupTools.DbTools.OpenConnection(); + Application app; + using (var uow = new AdoNetUnitOfWork(con)) + { + var repos = new AccountRepository(uow); + if (await repos.IsUserNameTakenAsync(model.UserName)) + return Redirect(Url.GetNextWizardStep()); + + account.SetVerifiedEmail(model.EmailAddress); + await repos.CreateAsync(account); + + var user = new User(account.Id, account.UserName) + { + EmailAddress = account.Email + }; + var userRepos = new UserRepository(uow); + await userRepos.CreateAsync(user); + + var repos2 = new ApplicationRepository(uow); + app = new Application(user.AccountId, "DemoApp") + { + ApplicationType = TypeOfApplication.DesktopApplication + }; + await repos2.CreateAsync(app); + + var tm = new ApplicationTeamMember(app.Id, account.Id, "System") + { + Roles = new[] { Domain.Core.Applications.ApplicationRole.Admin, Domain.Core.Applications.ApplicationRole.Member }, + UserName = account.UserName + }; + await repos2.CreateAsync(tm); + + uow.SaveChanges(); + } + + return Redirect(Url.GetNextWizardStep()); + } + catch (Exception ex) + { + ViewBag.Exception = ex; + ModelState.AddModelError("", ex.Message); + return View(model); + } + } + + private void SetStateFlag() + { + ViewBag.Exception = null; + ViewBag.AlreadyCreated = false; + using (var con = SetupTools.DbTools.OpenConnection()) + { + using (var uow = new AdoNetUnitOfWork(con)) + { + var id = uow.ExecuteScalar("SELECT TOP 1 Id FROM Accounts"); + if (id != null) + ViewBag.AlreadyCreated = true; + } + } + + if (!ViewBag.AlreadyCreated) + ViewBag.NextLink = null; + } + + public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + ViewBag.PrevLink = Url.GetPreviousWizardStepLink(); + ViewBag.NextLink = Url.GetNextWizardStepLink(); + Response.Headers.Add("Cache-Control", "no-cache, no-store"); + Response.Headers.Add("Expires", "-1"); + return base.OnActionExecutionAsync(context, next); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/BootController.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/BootController.cs new file mode 100644 index 00000000..44c3d988 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/BootController.cs @@ -0,0 +1,34 @@ +using System; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Areas.Installation.Controllers +{ + /// + /// Purpose is to be able to launch installation area and be able to use dependencies in the home controller + /// + + public class BootController : Controller + { + public ActionResult Index() + { + return RedirectToAction("Index", "Home"); + } + + [AllowAnonymous/*, Route("installation/{*url}")*/] + public ActionResult NoInstallation() + { + if (Request.Path.Value.EndsWith("/setup/activate", StringComparison.OrdinalIgnoreCase)) + return Redirect("~/"); + return View(); + } + + [AllowAnonymous] + public ActionResult ToInstall() + { + return RedirectToRoute(new { Controller = "Setup", Area = "Installation" }); + } + + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/MessagingController.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/MessagingController.cs new file mode 100644 index 00000000..491c33fc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/MessagingController.cs @@ -0,0 +1,68 @@ +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.App.Modules.Messaging.Commands; +using Coderr.Server.WebSite.Areas.Installation.Models; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Coderr.Server.WebSite.Areas.Installation.Controllers +{ + [Area("Installation")] + + public class MessagingController : Controller + { + private ConfigurationStore _configStore; + + public MessagingController(ConfigurationStore configStore) + { + _configStore = configStore; + } + + public ActionResult Email() + { + var model = new EmailViewModel(); + var settings = _configStore.Load(); + if (!string.IsNullOrEmpty(settings.SmtpHost)) + { + model.AccountName = settings.AccountName; + model.PortNumber = settings.PortNumber; + model.SmtpHost = settings.SmtpHost; + model.UseSSL = settings.UseSsl; + model.AccountPassword = settings.AccountPassword; + } + else + { + ViewBag.NextLink = ""; + } + + return View(model); + } + + [HttpPost] + public ActionResult Email(EmailViewModel model) + { + if (!ModelState.IsValid) + return View(model); + + var settings = new DotNetSmtpSettings + { + AccountName = model.AccountName, + PortNumber = model.PortNumber ?? 25, + AccountPassword = model.AccountPassword, + SmtpHost = model.SmtpHost, + UseSsl = model.UseSSL + }; + _configStore.Store(settings); + return Redirect(Url.GetNextWizardStep()); + } + + public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + Response.Headers.Add("Cache-Control", "no-cache, no-store"); + Response.Headers.Add("Expires", "-1"); + ViewBag.PrevLink = Url.GetPreviousWizardStepLink(); + ViewBag.NextLink = Url.GetNextWizardStepLink(); + return base.OnActionExecutionAsync(context, next); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/SetupController.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/SetupController.cs new file mode 100644 index 00000000..6b0ec448 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/SetupController.cs @@ -0,0 +1,212 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Infrastructure; +using Coderr.Server.Infrastructure.Configuration; +using Coderr.Server.WebSite.Areas.Installation.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Coderr.Server.WebSite.Areas.Installation.Controllers +{ + [Area("Installation")] + + public class SetupController : Controller + { + private ConfigurationStore _configStore; + + public SetupController(ConfigurationStore configStore) + { + _configStore = configStore; + } + + + [HttpPost] + [AllowAnonymous] + public ActionResult Activate() + { + if (!SetupTools.DbTools.IsConfigurationComplete(HostConfig.Instance.ConnectionString)) + { + return RedirectToAction("Completed", new + { + displayError = 1 + }); + } + + HostConfig.Instance.TriggeredConfigured(); + SetupTools.DbTools.MarkConfigurationAsComplete(); + return Redirect("~/"); + } + + public ActionResult Basics() + { + var model = new BasicsViewModel(); + var config = _configStore.Load(); + if (config.BaseUrl != null) + { + model.BaseUrl = config.BaseUrl.ToString(); + model.SupportEmail = config.SupportEmail; + } + else + { + model.BaseUrl = Request.GetDisplayUrl() + .Replace("installation/setup/basics/", "") + .Replace("localhost", "yourServerName"); + ViewBag.NextLink = ""; + } + + + return View(model); + } + + [HttpPost] + public ActionResult Basics(BasicsViewModel model) + { + if (model.BaseUrl.StartsWith("http://yourServerName", StringComparison.OrdinalIgnoreCase)) + ModelState.AddModelError("BaseUrl", "You must specify a correct server name in the URL, or all links in notification emails will be incorrect."); + if (model.BaseUrl.StartsWith("http://localhost", StringComparison.OrdinalIgnoreCase)) + ModelState.AddModelError("BaseUrl", "You must specify a correct server name in the URL, or all links in notification emails will be incorrect."); + if (model.BaseUrl.StartsWith("https://localhost", StringComparison.OrdinalIgnoreCase)) + ModelState.AddModelError("BaseUrl", "You must specify a correct server name in the URL, or all links in notification emails will be incorrect."); + + if (!ModelState.IsValid) + { + ViewBag.NextLink = ""; + return View(model); + } + + var settings = new BaseConfiguration(); + if (!model.BaseUrl.EndsWith("/")) + model.BaseUrl += "/"; + + if (model.BaseUrl.IndexOf("localhost", StringComparison.OrdinalIgnoreCase) != -1) + { + ModelState.AddModelError("BaseUrl", + "Use the servers real DNS name instead of 'localhost'. If you don't the Ajax request wont work as CORS would be enforced by IIS."); + return View(model); + } + settings.BaseUrl = new Uri(model.BaseUrl); + settings.SupportEmail = model.SupportEmail; + _configStore.Store(settings); + return Redirect(Url.GetNextWizardStep()); + } + + public ActionResult Completed(string displayError = null) + { + ViewBag.DisplayError = displayError == "1"; + HostConfig.Instance.TriggeredConfigured(); + SetupTools.DbTools.MarkConfigurationAsComplete(); + return View(); + } + + public ActionResult Stats() + { + return View(); + } + + public ActionResult Errors() + { + var model = new ErrorTrackingViewModel(); + var config = _configStore.Load(); + if (!string.IsNullOrEmpty(model.ContactEmail)) + { + model.ContactEmail = config.ContactEmail; + } + else + { + ViewBag.NextLink = ""; + } + + return View("ErrorTracking", model); + } + + [HttpPost] + public ActionResult Errors(ErrorTrackingViewModel model) + { + if (!ModelState.IsValid) + return View("ErrorTracking", model); + + var settings = new CoderrConfigSection + { + ActivateTracking = true, + ContactEmail = model.ContactEmail, + InstallationId = Guid.NewGuid().ToString("N") + }; + _configStore.Store(settings); + return Redirect(Url.GetNextWizardStep()); + } + + // GET: Installation/Home + public ActionResult Index() + { + ViewBag.Ready = HostConfig.Instance.IsConfigured; + return View(); + } + + + + [HttpPost] + public ActionResult Index(string key) + { + if (key == "changeThis") + { + var errMsg = "The configuration password can be found in appSettings.json."; + ModelState.AddModelError("", errMsg); + return View(); + } + + if (key != HostConfig.Instance.InstallationPassword) + { + var errMsg = "The configuration password can be found in appSettings.json."; + ModelState.AddModelError("", errMsg); + return View(); + } + + return Redirect(Url.GetNextWizardStep()); + } + + public ActionResult Support() + { + return View(new SupportViewModel()); + } + + [HttpPost] + public async Task Support(SupportViewModel model) + { + if (!ModelState.IsValid) + return View(model); + + try + { + var client = new HttpClient(); + var content = + new FormUrlEncodedContent(new[] + { + new KeyValuePair("EmailAddress", model.Email), + new KeyValuePair("CompanyName", model.CompanyName) + }); + await client.PostAsync("https://coderr.io/support/register/", content); + return Redirect(Url.GetNextWizardStep()); + } + catch (Exception ex) + { + ModelState.AddModelError("", ex.Message); + return View(model); + } + } + + public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + Response.Headers.Add("Cache-Control", "no-cache, no-store"); + Response.Headers.Add("Expires", "-1"); + ViewBag.PrevLink = Url.GetPreviousWizardStepLink(); + ViewBag.NextLink = Url.GetNextWizardStepLink(); + return base.OnActionExecutionAsync(context, next); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/SqlController.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/SqlController.cs new file mode 100644 index 00000000..07e015b1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Controllers/SqlController.cs @@ -0,0 +1,129 @@ +using System; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Infrastructure; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Configuration; + +namespace Coderr.Server.WebSite.Areas.Installation.Controllers +{ + [Area("Installation")] + + public class SqlController : Controller + { + private static int _counter; + private IConfiguration _config; + + public SqlController(IConfiguration config) + { + _config = config; + } + + [HttpPost] + public ActionResult Connection() + { + return RedirectToAction("Tables"); + } + + public ActionResult Index() + { + var constr = _config.GetConnectionString("Db"); + if (!string.IsNullOrEmpty(constr)) + { + ViewBag.ConnectionString = constr ?? ""; + } + else + { + ViewBag.ConnectionString = ""; + ViewBag.NextLink = ""; + } + + return View(); + } + + [HttpGet] + public ActionResult Tables() + { + ViewBag.GotException = false; + if (SetupTools.DbTools.IsTablesInstalled()) + { + ViewBag.GotTables = true; + } + else + { + ViewBag.GotTables = false; + ViewBag.NextLink = ""; + } + return View(); + } + + [HttpPost] + public ActionResult Tables(string go) + { + try + { + SetupTools.DbTools.CreateTables(); + return Redirect(Url.GetNextWizardStep()); + } + catch (Exception ex) + { + ViewBag.GotException = true; + ViewBag.GotTables = false; + ModelState.AddModelError("", ex.Message +"
Connection string: " + HostConfig.Instance.ConnectionString); + ViewBag.FullException = ex.ToString(); + return View(); + } + //return RedirectToRoute(new {Area = "Installation", Controller = "Setup", Action = "Done"}); + } + + public ActionResult Validate() + { + var constr = "Not set"; + try + { + constr = HostConfig.Instance.ConnectionString; + constr = ChangeConnectionTimeout(constr); + SetupTools.DbTools.TestConnection(constr); + return Content(@"{ ""result"": ""ok"" }", "application/json"); + } + catch (Exception ex) + { + var errMsg = ex.Message.Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "") + .Replace("\n", "\\n"); + return Json(new + { + result = "fail", + reason = errMsg + "
Connection string: " + constr, + attempt = ++_counter + }); + } + } + internal static string ChangeConnectionTimeout(string conStr) + { + var pos = conStr.IndexOf("Connect Timeout", StringComparison.OrdinalIgnoreCase); + if (pos != -1) + { + var pos2 = conStr.IndexOf(';', pos); + if (pos2 == -1) + { + conStr = conStr.Substring(0, pos) + "Connect Timeout=5"; + } + else + { + conStr = conStr.Substring(0, pos) + "Connect Timeout=5;" + conStr.Substring(pos2); + } + } + + return conStr; + } + public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + ViewBag.PrevLink = Url.GetPreviousWizardStepLink(); + ViewBag.NextLink = Url.GetNextWizardStepLink(); + return base.OnActionExecutionAsync(context, next); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/InstallAuthorizationFilter.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/InstallAuthorizationFilter.cs new file mode 100644 index 00000000..24c4a9bf --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/InstallAuthorizationFilter.cs @@ -0,0 +1,36 @@ +using Coderr.Server.Abstractions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.Configuration; + +namespace Coderr.Server.WebSite.Areas.Installation +{ + /// + /// Deny access to installation wizard once the application is configured + /// + public class InstallAuthorizationFilter : IAuthorizationFilter + { + private readonly IConfiguration _configuration; + + public InstallAuthorizationFilter(IConfiguration configuration) + { + _configuration = configuration; + } + + public void OnAuthorization(AuthorizationFilterContext context) + { + if (!context.HttpContext.Request.Path.Value.Contains("/installation")) + return; + + if (!HostConfig.Instance.IsConfigured) + return; + + context.Result = new ContentResult + { + StatusCode = 403, + Content = "The installation wizard has been disabled. Goto the root of the website.", + ContentType = "text/plain" + }; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/AccountViewModel.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/AccountViewModel.cs new file mode 100644 index 00000000..809209de --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/AccountViewModel.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Areas.Installation.Models +{ + public class AccountViewModel + { + [Required, StringLength(255)] + public string EmailAddress { get; set; } + + [Required, StringLength(40)] + public string Password { get; set; } + + [Required, StringLength(40)] + public string UserName { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/BasicsViewModel.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/BasicsViewModel.cs new file mode 100644 index 00000000..f3f45e93 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/BasicsViewModel.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Areas.Installation.Models +{ + public class BasicsViewModel + { + [Required, MinLength(8)] + public string BaseUrl { get; set; } + + [Required, EmailAddress] + public string SupportEmail { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/EmailViewModel.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/EmailViewModel.cs new file mode 100644 index 00000000..2a15aaad --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/EmailViewModel.cs @@ -0,0 +1,22 @@ +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Areas.Installation.Models +{ + public class EmailViewModel + { + [Display(Name = "Account Name")] + public string AccountName { get; set; } + + [Display(Name = "Account password")] + public string AccountPassword { get; set; } + + [Display(Name = "SMTP Port"), Required] + public int? PortNumber { get; set; } + + [Display(Name = "SMTP Host"), Required] + public string SmtpHost { get; set; } + + [Display(Name = "Use SSL")] + public bool UseSSL { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/ErrorTrackingViewModel.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/ErrorTrackingViewModel.cs new file mode 100644 index 00000000..3a234cb5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/ErrorTrackingViewModel.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Areas.Installation.Models +{ + public class ErrorTrackingViewModel + { + [Display(Name = "Contact email"), EmailAddress] + public string ContactEmail { get; set; } + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/QueueViewModel.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/QueueViewModel.cs new file mode 100644 index 00000000..acda16d8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/QueueViewModel.cs @@ -0,0 +1,24 @@ +namespace Coderr.Server.WebSite.Areas.Installation.Models +{ + public class QueueViewModel + { + public bool EventAuthentication { get; set; } + + public string EventQueue { get; set; } + + public bool EventTransactions { get; set; } + + public bool FeedbackAuthentication { get; set; } + + public string FeedbackQueue { get; set; } + + public bool FeedbackTransactions { get; set; } + + public bool ReportAuthentication { get; set; } + public string ReportQueue { get; set; } + + public bool ReportTransactions { get; set; } + + public bool UseSql { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Models/SupportViewModel.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/SupportViewModel.cs similarity index 76% rename from src/Server/OneTrueError.Web/Areas/Installation/Models/SupportViewModel.cs rename to src/Server/Coderr.Server.WebSite/Areas/Installation/Models/SupportViewModel.cs index 04612efa..659cce72 100644 --- a/src/Server/OneTrueError.Web/Areas/Installation/Models/SupportViewModel.cs +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Models/SupportViewModel.cs @@ -1,12 +1,12 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Installation.Models -{ - public class SupportViewModel - { - [EmailAddress] - public string Email { get; set; } - - public string CompanyName { get; set; } - } +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Areas.Installation.Models +{ + public class SupportViewModel + { + [EmailAddress] + public string Email { get; set; } + + public string CompanyName { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Account/Admin.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Account/Admin.cshtml new file mode 100644 index 00000000..f9593b85 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Account/Admin.cshtml @@ -0,0 +1,65 @@ +@model Coderr.Server.WebSite.Areas.Installation.Models.AccountViewModel +@{ + ViewBag.Title = "Installation - Account"; +} +
+
+

Account creation

+

You need to create an account to be able to login. You can at a later point invite other users to coderr.

+ @if (ViewBag.AlreadyCreated) + { +
+ Account have already been created. +
+ @Html.Raw(ViewBag.PrevLink) + @Html.Raw(ViewBag.NextLink) + } + else + { +
+ @Html.ValidationSummary(false) +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ @Html.Raw(ViewBag.PrevLink) + + @Html.Raw(ViewBag.NextLink) +
+
+ } + @if (ViewBag.Exception != null) + { +

Error

+
@ViewBag.Exception
+ } +
+
+@section scripts +{ + + +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Messaging/Email.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Messaging/Email.cshtml new file mode 100644 index 00000000..072600b9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Messaging/Email.cshtml @@ -0,0 +1,58 @@ +@model Coderr.Server.WebSite.Areas.Installation.Models.EmailViewModel +@{ + ViewBag.Title = "Installation - Email configuration"; +} +
+
+ +

Email configuration

+

+ Coderr can send email notifcations upon different types of events (and password resets etc). To do this, Community Server need + to have a SMTP account for mailing. +

+
+ @Html.ValidationSummary(false) +
+ + + Use SSL +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+
+ @Html.Raw(ViewBag.PrevLink) + + @Html.Raw(ViewBag.NextLink) +
+
+
+@section scripts +{ + + +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Basics.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Basics.cshtml new file mode 100644 index 00000000..6f6bcc4c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Basics.cshtml @@ -0,0 +1,29 @@ +@model Coderr.Server.WebSite.Areas.Installation.Models.BasicsViewModel +@{ + ViewBag.Title = "Installation - Basics"; +} + +
+
+

Base configuration

+
+ @Html.ValidationSummary(false) +
+ +
+ Address used when visiting this site. The address is used in notification emails. http://localhost doesn't just cut it. +
+
+ +
+ Used by your users when they need support using coderr, for instance for account troubles. Also used as sender in outbound emails (notifications to your users). +
+ +
+ @Html.Raw(ViewBag.PrevLink) + + @Html.Raw(ViewBag.NextLink) +
+
+
+
\ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Completed.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Completed.cshtml new file mode 100644 index 00000000..6cd52892 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Completed.cshtml @@ -0,0 +1,13 @@ +@using Coderr.Server.Abstractions +@{ + ViewBag.Title = "Installation - Completed"; +} +
+
+ +

Congratulations!

+ +

The setup is now completed. Click on the button below to login. This installation wizard is now deactivated.

+ Start using Coderr +
+
\ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/ErrorTracking.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/ErrorTracking.cshtml new file mode 100644 index 00000000..b182c34e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/ErrorTracking.cshtml @@ -0,0 +1,27 @@ +@model Coderr.Server.WebSite.Areas.Installation.Models.ErrorTrackingViewModel +@{ + ViewBag.Title = "Installation - Error tracking"; +} +
+
+ +

Error tracking

+

+ We have activated Coderr in the Community Server to send us bugs as they occur. It is our own way to practice what we preach about error handling and helps us to correct our code faster. Further, with rich context information provided by Coderr, it enables us understand the error better. +

+

+ If you want to receive notification when the errors in your installation have been corrected, please provide an email address. +

+
+ @Html.ValidationSummary(false) +
+ + +
+
+ @Html.Raw(ViewBag.PrevLink) + + @Html.Raw(ViewBag.NextLink) +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Index.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Index.cshtml new file mode 100644 index 00000000..443b8914 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Index.cshtml @@ -0,0 +1,29 @@ +@{ + ViewBag.Title = "Installation - First step"; +} +
+
+ +

Welcome to the Coderr Community Server

+

+ This guide will help you configure the server. Most of the settings are stored in the Settings table in the database. +

+ + +

Verification

+

+ To start, you must enter the password from the General:InstallationPassword setting in appsettings.json. +

+
+ @Html.ValidationSummary() +
+ +
+
+ +
+ For a zero configuration experience, try our service Coderr Cloud - free up to five users. +
+
+
+
\ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Stats.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Stats.cshtml new file mode 100644 index 00000000..21aa025d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Stats.cshtml @@ -0,0 +1,29 @@ +@{ + ViewBag.Title = "Installation - Usage statistics"; +} +
+
+ +

Usage statistics

+

+ We need to collect anonymous usage statistics to understand how Coderr is used and how we can make it better. + The data we collect are specified below and by continuing with the setup you agree for us to collect it. + This data is anonymous and confidential and never shared outside our company. +

+
{
+    Guid InstallationId; // Cannot be used to identify you, your company or your servers.
+    YearMonth DateTime;  // year+month for the usage
+    Applications[]:      // One entry per application that you have added to Coderr
+    {
+        ApplicationdId: int; // Id of a specific application
+        IncidentCount: int;  // Number of new incidents created the given month
+        ReportCount: int;    // Number of error reports received the given month
+        ClosedCount: int;    // Number of errors that was corrected/solved this month
+        ReOpened: int;       // Number of errors that was repopened this month
+    }
+}
+ @Html.Raw(ViewBag.PrevLink) + @Html.Raw(ViewBag.NextLink) +
+
+
\ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Support.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Support.cshtml new file mode 100644 index 00000000..8acbbd8b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Setup/Support.cshtml @@ -0,0 +1,24 @@ +@model Coderr.Server.WebSite.Areas.Installation.Models.SupportViewModel +@{ + ViewBag.Title = "Installation - Support"; +} +
+
+

Getting started assistance

+

We offer free assistance during the first month of usage. We want to make sure there is nothing on our end or from Coderr that stops you from getting to know Coderr. Sign up to our free service by entering your contact email which will only be used to assist you with Coderr.

+
+ @Html.ValidationSummary(false) +
+ + +
+
(Leave the fields empty if you do not want to sign up)
+
 
+
+ @Html.Raw(ViewBag.PrevLink) + + @Html.Raw(ViewBag.NextLink) +
+
+
+
\ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Shared/_JQueryReplacement.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Shared/_JQueryReplacement.cshtml new file mode 100644 index 00000000..21e44186 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Shared/_JQueryReplacement.cshtml @@ -0,0 +1,79 @@ + \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Shared/_Layout.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Shared/_Layout.cshtml new file mode 100644 index 00000000..f898008c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Shared/_Layout.cshtml @@ -0,0 +1,52 @@ + + + + + + @ViewData["Title"] - Coderr installation wizard + + + + @* + + *@ + + +
+
+ @RenderBody() +
+
+ @RenderSection("scripts", required: false) + + diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Sql/Index.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Sql/Index.cshtml new file mode 100644 index 00000000..a70bac24 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Sql/Index.cshtml @@ -0,0 +1,97 @@ +@using Coderr.Server.Abstractions +@{ + ViewBag.Title = "Installation - Database configuration"; +} +
+
+
+

Database configuration

+

+ It's time to configure the database. To do that you need + to start by specifying which database to use. We expect that you have created + a database and configured an account for it. +

+

+ Modify the connectionString named 'Db' in appsettings.json. Click on 'Test Connection' to make sure that it works. +

+
+
+

+ Configured connection string: +

+ + @HostConfig.Instance.ConnectionString + +

+ @Html.Raw(ViewBag.PrevLink) + + @Html.Raw(ViewBag.NextLink) +
+
+ + +
+
+

Limitation

+

+ + Currently only Microsoft SQL Server 2012 and above is supported. Need any other DB? Feel free to Contribute + by taking the SqlServer class library and convert it to a library for your favorite DB engine. + +

+

Example

+
Data Source=.;Initial Catalog=coderr;Integrated Security=True;Connect Timeout=30;
+

Tip!

+

+ Do you want to give permissions to the IIS app pool? Add "IIS APPPOOL\YourAppPool" as the windows account in SQL Server Management Studio. +

+

+ For instance IIS APPPOOL\DefaultAppPool. +

+
+
+
+@section scripts +{ + + +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Sql/Tables.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Sql/Tables.cshtml new file mode 100644 index 00000000..f836a6de --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/Sql/Tables.cshtml @@ -0,0 +1,47 @@ +@{ ViewBag.Title = "Tables"; } + +
+
+

Tables

+ +

It's time to install all the tables. Keep your fingers crossed and press "Create Tables"

+ + @if (ViewBag.GotTables) + { +
+ Tables have already been added/updated. +
+ @Html.Raw(ViewBag.PrevLink) + @Html.Raw(ViewBag.NextLink) } + else + { +
+ @Html.ValidationSummary(true) + + @Html.Raw(ViewBag.PrevLink) + + @Html.Raw(ViewBag.NextLink) +
} + @if (ViewBag.GotException) + { +

Error details

+
+
@ViewBag.FullException
+
+
} +
+
+@section scripts +{ + + +} diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/_ViewImports.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/_ViewImports.cshtml new file mode 100644 index 00000000..9ec2efc9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using Microsoft.AspNetCore.Identity +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/_ViewStart.cshtml b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/_ViewStart.cshtml new file mode 100644 index 00000000..0541200a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "/Areas/Installation/Views/Shared/_Layout.cshtml"; +} diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/WizardStepInfo.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/WizardStepInfo.cs new file mode 100644 index 00000000..eb719917 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/WizardStepInfo.cs @@ -0,0 +1,32 @@ +using System; +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Areas.Installation +{ + public class WizardStepInfo + { + public WizardStepInfo(string name, string virtualPath) + { + Name = name; + VirtualPath = virtualPath; + } + + public string Name { get; set; } + + public string VirtualPath { get; set; } + + public bool IsForAbsolutePath(string currentPath, IUrlHelper helper) + { + currentPath = currentPath.TrimEnd('/'); + + var stepPath = helper.Content(VirtualPath).TrimEnd('/'); + if (stepPath.Equals(currentPath, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + stepPath = VirtualPath.Replace("~", "").TrimEnd('/'); + return currentPath.Equals(stepPath, StringComparison.OrdinalIgnoreCase); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Areas/Installation/WizardSteps.cs b/src/Server/Coderr.Server.WebSite/Areas/Installation/WizardSteps.cs new file mode 100644 index 00000000..3cb276cb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Areas/Installation/WizardSteps.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Areas.Installation +{ + public static class WizardSteps + { + private static readonly WizardStepInfo[] Steps = + { + new WizardStepInfo("Introduction", "~/installation"), + new WizardStepInfo("Configure database", "~/installation/sql/"), + new WizardStepInfo("Create tables", "~/installation/sql/tables/"), + new WizardStepInfo("Base configuration", "~/installation/setup/basics/"), + new WizardStepInfo("Error tracking", "~/installation/setup/errors/"), + new WizardStepInfo("Create admin account", "~/installation/account/admin/"), + new WizardStepInfo("Mail settings", "~/installation/messaging/email/"), + new WizardStepInfo("Support", "~/installation/setup/support"), + new WizardStepInfo("Usage statistics", "~/installation/setup/stats/"), + new WizardStepInfo("Completed", "~/installation/setup/completed/") + }; + + + public static string GetNextWizardStep(this IUrlHelper urlHelper) + { + var index = FindCurrentIndex(urlHelper); + if (index == -1) + return null; + + if (index < Steps.Length - 1) + index++; + + var step = Steps[index]; + return urlHelper.Content(step.VirtualPath); + } + + public static string GetNextWizardStepLink(this IUrlHelper urlHelper) + { + var index = FindCurrentIndex(urlHelper); + if (index == -1) + return ""; + if (index < Steps.Length - 1) + index++; + + var step = Steps[index]; + return + $@"{step.Name} >>"; + } + + public static string GetPreviousWizardStepLink(this IUrlHelper urlHelper) + { + var index = FindCurrentIndex(urlHelper); + if (index == -1) + return ""; + if (index > 0) + index--; + + var step = Steps[index]; + return + $@"<< {step.Name}"; + } + + private static int FindCurrentIndex(IUrlHelper urlHelper) + { + var currentPath = urlHelper.ActionContext.HttpContext.Request.Path.Value; + for (var i = 0; i < Steps.Length; i++) + { + if (Steps[i].IsForAbsolutePath(currentPath, urlHelper)) + return i; + } + // in ASP.NET Core, the virtual path is in PathBase and the rest in Path + // this we only need to check if the path is root (while the wizard link is for ./installation) + // It's "/" when there is no virtual directory and "" when there is one. Go figure. + if (urlHelper.ActionContext.HttpContext.Request.Path == "" || urlHelper.ActionContext.HttpContext.Request.Path == "/") + return 0; + + return -1; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/.editorconfig b/src/Server/Coderr.Server.WebSite/ClientApp/.editorconfig new file mode 100644 index 00000000..74c81669 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + +[*.cs] +csharp_prefer_braces = true:error diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/.gitignore b/src/Server/Coderr.Server.WebSite/ClientApp/.gitignore new file mode 100644 index 00000000..bc1ce71d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/.gitignore @@ -0,0 +1,41 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/dist-server +/tmp +/out-tsc + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.angular/cache +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/README.md b/src/Server/Coderr.Server.WebSite/ClientApp/README.md new file mode 100644 index 00000000..a61a5422 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/README.md @@ -0,0 +1,27 @@ +# CoderrAngular2 + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 6.0.0. + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/angular.json b/src/Server/Coderr.Server.WebSite/ClientApp/angular.json new file mode 100644 index 00000000..4385bd16 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/angular.json @@ -0,0 +1,157 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "cli": { + "analytics": false + }, + "version": 1, + "newProjectRoot": "projects", + "projects": { + "coderr-frontend": { + "root": "", + "sourceRoot": "src", + "projectType": "application", + "prefix": "app", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + }, + "@schematics/angular:application": { + "strict": true + } + }, + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "progress": false, + "outputPath": "dist", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.app.json", + "assets": [ "src/assets" ], + "styles": [ + "src/styles/site.scss", + "node_modules/ngx-toastr/toastr.css" + ], + "scripts": [ + ], + "aot": false, + "vendorChunk": true, + "extractLicenses": false, + "buildOptimizer": false, + "sourceMap": true, + "optimization": false, + "namedChunks": true + }, + "configurations": { + "production": { + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true + } + }, + "defaultConfiguration": "" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "browserTarget": "coderr-frontend:build", + "disableHostCheck": true + }, + "configurations": { + "production": { + "browserTarget": "coderr-frontend:build:production" + } + } + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "coderr-frontend:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "src/test.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "src/tsconfig.spec.json", + "karmaConfig": "src/karma.conf.js", + "styles": [ "src/styles/styles.css" ], + "scripts": [], + "assets": [ "src/assets" ] + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": [ "src/tsconfig.app.json", "src/tsconfig.spec.json" ], + "exclude": [ "**/node_modules/**" ] + } + }, + "server": { + "builder": "@angular-devkit/build-angular:server", + "options": { + "outputPath": "dist-server", + "main": "src/main.ts", + "tsConfig": "src/tsconfig.server.json", + "sourceMap": true, + "optimization": false + }, + "configurations": { + "dev": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": true + }, + "production": { + "optimization": true, + "outputHashing": "all", + "sourceMap": false, + "namedChunks": false, + "extractLicenses": true, + "vendorChunk": false + } + }, + "defaultConfiguration": "" + } + } + }, + "coderr-frontend-e2e": { + "root": "e2e/", + "projectType": "application", + "architect": { + "e2e": { + "builder": "@angular-devkit/build-angular:protractor", + "options": { + "protractorConfig": "e2e/protractor.conf.js", + "devServerTarget": "coderr-frontend:serve" + } + }, + "lint": { + "builder": "@angular-devkit/build-angular:tslint", + "options": { + "tsConfig": "e2e/tsconfig.e2e.json", + "exclude": [ "**/node_modules/**" ] + } + } + } + } + }, + "defaultProject": "coderr-frontend" +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/browserslist b/src/Server/Coderr.Server.WebSite/ClientApp/browserslist new file mode 100644 index 00000000..679e4a8d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/browserslist @@ -0,0 +1,10 @@ +# This file is currently used by autoprefixer to adjust CSS to support the below specified browsers +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries +# For IE 9-11 support, please uncomment the last line of the file and adjust as needed +> 0.5% +last 2 versions +Firefox ESR +not dead +# IE 9-11 +not IE 11 diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/e2e/protractor.conf.js b/src/Server/Coderr.Server.WebSite/ClientApp/e2e/protractor.conf.js new file mode 100644 index 00000000..d60eff06 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/e2e/protractor.conf.js @@ -0,0 +1,28 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require("jasmine-spec-reporter"); + +exports.config = { + allScriptsTimeout: 11000, + specs: ["./src/**/*.e2e-spec.ts"], + capabilities: { + browserName: "chrome" + }, + directConnect: true, + baseUrl: "http://localhost:4200/", + framework: "jasmine", + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + onPrepare() { + require("ts-node").register({ + project: require("path").join(__dirname, "./tsconfig.e2e.json") + }); + jasmine + .getEnv() + .addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/e2e/src/app.e2e-spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/e2e/src/app.e2e-spec.ts new file mode 100644 index 00000000..5b3b4b27 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/e2e/src/app.e2e-spec.ts @@ -0,0 +1,14 @@ +import { AppPage } from './app.po'; + +describe('App', () => { + let page: AppPage; + + beforeEach(() => { + page = new AppPage(); + }); + + it('should display welcome message', () => { + page.navigateTo(); + expect(page.getMainHeading()).toEqual('Hello, world!'); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/e2e/src/app.po.ts b/src/Server/Coderr.Server.WebSite/ClientApp/e2e/src/app.po.ts new file mode 100644 index 00000000..24bc8b3c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/e2e/src/app.po.ts @@ -0,0 +1,11 @@ +import { browser, by, element } from 'protractor'; + +export class AppPage { + navigateTo() { + return browser.get('/'); + } + + getMainHeading() { + return element(by.css('app-root h1')).getText(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/e2e/tsconfig.e2e.json b/src/Server/Coderr.Server.WebSite/ClientApp/e2e/tsconfig.e2e.json new file mode 100644 index 00000000..a6dd6220 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/e2e/tsconfig.e2e.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/app", + "module": "commonjs", + "target": "es5", + "types": [ + "jasmine", + "jasminewd2", + "node" + ] + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/ngserve.cmd b/src/Server/Coderr.Server.WebSite/ClientApp/ngserve.cmd new file mode 100644 index 00000000..2879b1a8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/ngserve.cmd @@ -0,0 +1,2 @@ +@echo ** Angular Live Development Server is listening on localhost:%~2, open your browser on http://localhost:%~2/ ** +ng serve %1 %~2 diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/package-lock.json b/src/Server/Coderr.Server.WebSite/ClientApp/package-lock.json new file mode 100644 index 00000000..0d8a673b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/package-lock.json @@ -0,0 +1,23651 @@ +{ + "name": "coderr-frontend", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "coderr-frontend", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^13.3.0", + "@angular/common": "^13.3.0", + "@angular/core": "13.3.0", + "@angular/forms": "^13.3.0", + "@angular/platform-browser": "^13.3.0", + "@angular/platform-browser-dynamic": "^13.3.0", + "@angular/platform-server": "^13.3.0", + "@angular/router": "^13.3.0", + "@microsoft/signalr": "^6.0.2", + "@popperjs/core": "^2.11.4", + "apexcharts": "^3.29.0", + "bootstrap": "^4.6.1", + "core-js": "^3.19.0", + "jquery": "^3.6.0", + "jwt-decode": "^3.1.2", + "ngx-toastr": "^14.1.4", + "oidc-client": "^1.11.5", + "reflect-metadata": "^0.1.13", + "rxjs": "^6.6.7", + "service": "^0.1.4", + "toastr": "^2.1.4", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^13.3.0", + "@angular/cli": "^13.3.0", + "@angular/compiler": "^13.3.0", + "@angular/compiler-cli": "^13.3.0", + "@angular/language-service": "^13.3.0", + "@types/jasmine": "~3.4.4", + "@types/jasminewd2": "^2.0.10", + "@types/node": "~12.11.6", + "codelyzer": "^6.0.2", + "jasmine-core": "^3.10.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "^6.3.7", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage-istanbul-reporter": "~2.1.0", + "karma-jasmine": "~2.0.1", + "karma-jasmine-html-reporter": "^1.7.0", + "typescript": "^4.5.5" + }, + "optionalDependencies": { + "protractor": "^7.0.0", + "ts-node": "~8.4.1", + "tslint": "~5.20.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-1.1.1.tgz", + "integrity": "sha512-YVAcA4DKLOj296CF5SrQ8cYiMRiUGc2sqFpLxsDGWE34suHqhGP/5yMsDHKsrh8hs8I5TiRVXNwKPWQpX3iGjw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "sourcemap-codec": "1.4.8" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1303.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1303.0.tgz", + "integrity": "sha512-kTcKB917ICA8j53SGo4gn+qAlzx8si+iHnOTbp5QlMr7qt/Iz07SVVI8mRlMD6c6lr7eE/fVlCLzEZ1+WCQpTA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "13.3.0", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.3.0.tgz", + "integrity": "sha512-3Ji7EeqGHj7i1Jgmeo3aIEXsnfKyFeQPpl65gcYmHwj5dP4lZzLSU4rMaWWUKksccgqCUXgPI2vKePTPazmikg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "1.1.1", + "@angular-devkit/architect": "0.1303.0", + "@angular-devkit/build-webpack": "0.1303.0", + "@angular-devkit/core": "13.3.0", + "@babel/core": "7.16.12", + "@babel/generator": "7.16.8", + "@babel/helper-annotate-as-pure": "7.16.7", + "@babel/plugin-proposal-async-generator-functions": "7.16.8", + "@babel/plugin-transform-async-to-generator": "7.16.8", + "@babel/plugin-transform-runtime": "7.16.10", + "@babel/preset-env": "7.16.11", + "@babel/runtime": "7.16.7", + "@babel/template": "7.16.7", + "@discoveryjs/json-ext": "0.5.6", + "@ngtools/webpack": "13.3.0", + "ansi-colors": "4.1.1", + "babel-loader": "8.2.3", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.9.1", + "cacache": "15.3.0", + "circular-dependency-plugin": "5.2.2", + "copy-webpack-plugin": "10.2.1", + "core-js": "3.20.3", + "critters": "0.0.16", + "css-loader": "6.5.1", + "esbuild-wasm": "0.14.22", + "glob": "7.2.0", + "https-proxy-agent": "5.0.0", + "inquirer": "8.2.0", + "jsonc-parser": "3.0.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.2", + "less-loader": "10.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.0", + "mini-css-extract-plugin": "2.5.3", + "minimatch": "3.0.4", + "open": "8.4.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "6.0.1", + "piscina": "3.2.0", + "postcss": "8.4.5", + "postcss-import": "14.0.2", + "postcss-loader": "6.2.1", + "postcss-preset-env": "7.2.3", + "regenerator-runtime": "0.13.9", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.49.0", + "sass-loader": "12.4.0", + "semver": "7.3.5", + "source-map-loader": "3.0.1", + "source-map-support": "0.5.21", + "stylus": "0.56.0", + "stylus-loader": "6.2.0", + "terser": "5.11.0", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.3.1", + "webpack": "5.70.0", + "webpack-dev-middleware": "5.3.0", + "webpack-dev-server": "4.7.3", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.14.22" + }, + "peerDependencies": { + "@angular/compiler-cli": "^13.0.0 || ^13.3.0-rc.0", + "@angular/localize": "^13.0.0 || ^13.3.0-rc.0", + "@angular/service-worker": "^13.0.0 || ^13.3.0-rc.0", + "karma": "^6.3.0", + "ng-packagr": "^13.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.4.3 <4.7" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/core-js": { + "version": "3.20.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz", + "integrity": "sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag==", + "dev": true, + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1303.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1303.0.tgz", + "integrity": "sha512-a+Veg2oYn3RM2Kl148BReuONmD1kjbbYBnMUVi8nD6rvJPStFZkqN5s5ZkYybKeWnzMGaB3VasKR88z5XeH22A==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1303.0", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.0.tgz", + "integrity": "sha512-8YrreVbWlJVZnk5zs4vfkRItrPEtWhUcxWOBfYT/Kwu4FwJVAnNuhJAxxXOAQ2Ckd7cv30Idh/RFVLbTZ5Gs9w==", + "dev": true, + "dependencies": { + "ajv": "8.9.0", + "ajv-formats": "2.1.1", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.0.tgz", + "integrity": "sha512-hq7tqnB3uVT/iDgqWWZ4kvnijeAcgd4cfLzZiCPaYn1nuhZf0tWsho6exhJ/odMZHvVp7w8OibqWiUKxNY9zHA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "13.3.0", + "jsonc-parser": "3.0.0", + "magic-string": "0.25.7", + "ora": "5.4.1", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/animations": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-13.3.0.tgz", + "integrity": "sha512-q7hkImhHCv0QdriR8HOFhsAW05QDmvapcHrBv3y862LUTR5e90/+81RYuwFuKX1lk/sa7LiHlHHWC7oCspzr2Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/core": "13.3.0" + } + }, + "node_modules/@angular/cli": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.0.tgz", + "integrity": "sha512-2qCKP/QsyxrJnpd3g4P/iTQ4TjI04N8r+bG5YLLfudoMDsQ/Ti4ogdI7PBeG2IMbRylZW9XLjHraWG42+Y9tWw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@angular-devkit/architect": "0.1303.0", + "@angular-devkit/core": "13.3.0", + "@angular-devkit/schematics": "13.3.0", + "@schematics/angular": "13.3.0", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.1", + "debug": "4.3.3", + "ini": "2.0.0", + "inquirer": "8.2.0", + "jsonc-parser": "3.0.0", + "npm-package-arg": "8.1.5", + "npm-pick-manifest": "6.1.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "12.0.3", + "resolve": "1.22.0", + "semver": "7.3.5", + "symbol-observable": "4.0.0", + "uuid": "8.3.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-13.3.0.tgz", + "integrity": "sha512-yl09TWBmz++Z3MKjzZIwU2wZHiedCn1DjGILjjNXegHFOfINRHiqLhHca4kGWFcTsdvcuEhd9Hk9JATqi45rjg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/core": "13.3.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-13.3.0.tgz", + "integrity": "sha512-oeUvaBOVpey2G1I5fWZa3JcyRuBQ3dAeRay5UtQhu1Xw2L8jd2tYkbZb1XOgP9J1/Ma4LO62pjSaOpR2EtO5ww==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + } + }, + "node_modules/@angular/compiler-cli": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-13.3.0.tgz", + "integrity": "sha512-f9m55YejHJNIDTwHyGwf3wn5AvZepDfdAgeJP0Re4XmO1mf/Z9Ob5mJP5Q1yLNhqk0DlURWsZ1CbJqufPXMTbQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.17.2", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.26.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "sourcemap-codec": "^1.4.8", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/main-ngcc.js" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/compiler": "13.3.0", + "typescript": ">=4.4.2 <4.7" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@ampproject/remapping": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", + "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz", + "integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.7", + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helpers": "^7.17.8", + "@babel/parser": "^7.17.8", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular/compiler-cli/node_modules/magic-string": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.1.tgz", + "integrity": "sha512-ndThHmvgtieXe8J/VGPjG+Apu7v7ItcD5mhEIvOscWjPF/ccOiLxHaSuCAS2G+3x4GKsAbT8u7zdyamupui8Tg==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.8" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular/compiler-cli/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@angular/core": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-13.3.0.tgz", + "integrity": "sha512-ZnuIMEK8YFBtthNqrxapYolMp6qRy4Yp/VG+M11YNiuBp/BoYYDjTaknwO8vu36Cn6372zWjcibsknkZMjdBkg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.11.4" + } + }, + "node_modules/@angular/forms": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-13.3.0.tgz", + "integrity": "sha512-eBySo+B3/AV+p3SmD15Tg41N+SoxYPyqGnlCTR+jSrFis5ZZNWf0kKpIKhJhW2taRq6K+1o3KcA0W9bnphrZDQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/common": "13.3.0", + "@angular/core": "13.3.0", + "@angular/platform-browser": "13.3.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/language-service": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-13.3.0.tgz", + "integrity": "sha512-XzfamYk39h+ASxb2ycZKfn9nITmns+jTV+DPrFAY1BoW+4x3tAoqt3/CkdJn8UaCePswKXckDQ6y9i109TfddQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-13.3.0.tgz", + "integrity": "sha512-OgNVgRtqTPxzItZbJVe4NmSYKDLEKQYjGulStWl4ycQTsOKteF+sJi8gU5BvEU/KQNZItYnIQxMqTsFyS7xlRQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/animations": "13.3.0", + "@angular/common": "13.3.0", + "@angular/core": "13.3.0" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-13.3.0.tgz", + "integrity": "sha512-7/r79Yn8SDH8t0/fJ26PmScm/S1JZ9hxjC8IoROdyC5xBrSGrp946mIKE/4/813zmF8uPj2lveV9p/XiKTbxSw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/common": "13.3.0", + "@angular/compiler": "13.3.0", + "@angular/core": "13.3.0", + "@angular/platform-browser": "13.3.0" + } + }, + "node_modules/@angular/platform-server": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-13.3.0.tgz", + "integrity": "sha512-kfKyFi77f4MfcVfsv2O9icYz8FdqTjFdU1C8YDSC0OTgy4/QmwQ3lboZwcsvMRfF/aAb3liiFhPvNAjC2CYSEw==", + "dependencies": { + "domino": "^2.1.2", + "tslib": "^2.3.0", + "xhr2": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/animations": "13.3.0", + "@angular/common": "13.3.0", + "@angular/compiler": "13.3.0", + "@angular/core": "13.3.0", + "@angular/platform-browser": "13.3.0", + "@angular/platform-browser-dynamic": "13.3.0" + } + }, + "node_modules/@angular/router": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-13.3.0.tgz", + "integrity": "sha512-Kz657mtycup+s9emRH66etkBobAF26h3UDXE9pnjUM6MuVTA38P31WyTWKyWJVk8Oruxm/hTHZZBfI88o9/1sA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0" + }, + "peerDependencies": { + "@angular/common": "13.3.0", + "@angular/core": "13.3.0", + "@angular/platform-browser": "13.3.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "devOptional": true, + "dependencies": { + "@babel/highlight": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", + "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.16.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", + "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.16.8", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.16.7", + "@babel/parser": "^7.16.12", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.10", + "@babel/types": "^7.16.8", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/core/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", + "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.8", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz", + "integrity": "sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA==", + "dev": true, + "dependencies": { + "@babel/helper-explode-assignable-expression": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz", + "integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.17.5", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz", + "integrity": "sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-member-expression-to-functions": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", + "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "regexpu-core": "^5.0.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", + "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-explode-assignable-expression": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz", + "integrity": "sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", + "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", + "dev": true, + "dependencies": { + "@babel/helper-get-function-arity": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-get-function-arity": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", + "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", + "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", + "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz", + "integrity": "sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-wrap-function": "^7.16.8", + "@babel/types": "^7.16.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", + "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-member-expression-to-functions": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", + "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "devOptional": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz", + "integrity": "sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz", + "integrity": "sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", + "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "devOptional": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz", + "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz", + "integrity": "sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz", + "integrity": "sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz", + "integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-remap-async-to-generator": "^7.16.8", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz", + "integrity": "sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-class-static-block": { + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz", + "integrity": "sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.17.6", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-proposal-dynamic-import": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz", + "integrity": "sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-namespace-from": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz", + "integrity": "sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-json-strings": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz", + "integrity": "sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz", + "integrity": "sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz", + "integrity": "sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-numeric-separator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz", + "integrity": "sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-object-rest-spread": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz", + "integrity": "sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.0", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-catch-binding": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz", + "integrity": "sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-optional-chaining": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz", + "integrity": "sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-methods": { + "version": "7.16.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz", + "integrity": "sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.16.10", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz", + "integrity": "sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz", + "integrity": "sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", + "integrity": "sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz", + "integrity": "sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-remap-async-to-generator": "^7.16.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz", + "integrity": "sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz", + "integrity": "sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", + "integrity": "sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz", + "integrity": "sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz", + "integrity": "sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz", + "integrity": "sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz", + "integrity": "sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz", + "integrity": "sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz", + "integrity": "sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz", + "integrity": "sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz", + "integrity": "sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz", + "integrity": "sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz", + "integrity": "sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz", + "integrity": "sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz", + "integrity": "sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz", + "integrity": "sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz", + "integrity": "sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz", + "integrity": "sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz", + "integrity": "sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz", + "integrity": "sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz", + "integrity": "sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz", + "integrity": "sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q==", + "dev": true, + "dependencies": { + "regenerator-transform": "^0.14.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz", + "integrity": "sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.10.tgz", + "integrity": "sha512-9nwTiqETv2G7xI4RvXHNfpGdr8pAA+Q/YtN3yLK7OoK7n9OibVm/xymJ838a9A6E/IciOLPj82lZk0fW6O4O7w==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", + "integrity": "sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz", + "integrity": "sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz", + "integrity": "sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz", + "integrity": "sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz", + "integrity": "sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz", + "integrity": "sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz", + "integrity": "sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.16.11", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.11.tgz", + "integrity": "sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.16.8", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-option": "^7.16.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.7", + "@babel/plugin-proposal-async-generator-functions": "^7.16.8", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-class-static-block": "^7.16.7", + "@babel/plugin-proposal-dynamic-import": "^7.16.7", + "@babel/plugin-proposal-export-namespace-from": "^7.16.7", + "@babel/plugin-proposal-json-strings": "^7.16.7", + "@babel/plugin-proposal-logical-assignment-operators": "^7.16.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", + "@babel/plugin-proposal-numeric-separator": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.16.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", + "@babel/plugin-proposal-optional-chaining": "^7.16.7", + "@babel/plugin-proposal-private-methods": "^7.16.11", + "@babel/plugin-proposal-private-property-in-object": "^7.16.7", + "@babel/plugin-proposal-unicode-property-regex": "^7.16.7", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-async-to-generator": "^7.16.8", + "@babel/plugin-transform-block-scoped-functions": "^7.16.7", + "@babel/plugin-transform-block-scoping": "^7.16.7", + "@babel/plugin-transform-classes": "^7.16.7", + "@babel/plugin-transform-computed-properties": "^7.16.7", + "@babel/plugin-transform-destructuring": "^7.16.7", + "@babel/plugin-transform-dotall-regex": "^7.16.7", + "@babel/plugin-transform-duplicate-keys": "^7.16.7", + "@babel/plugin-transform-exponentiation-operator": "^7.16.7", + "@babel/plugin-transform-for-of": "^7.16.7", + "@babel/plugin-transform-function-name": "^7.16.7", + "@babel/plugin-transform-literals": "^7.16.7", + "@babel/plugin-transform-member-expression-literals": "^7.16.7", + "@babel/plugin-transform-modules-amd": "^7.16.7", + "@babel/plugin-transform-modules-commonjs": "^7.16.8", + "@babel/plugin-transform-modules-systemjs": "^7.16.7", + "@babel/plugin-transform-modules-umd": "^7.16.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.8", + "@babel/plugin-transform-new-target": "^7.16.7", + "@babel/plugin-transform-object-super": "^7.16.7", + "@babel/plugin-transform-parameters": "^7.16.7", + "@babel/plugin-transform-property-literals": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-reserved-words": "^7.16.7", + "@babel/plugin-transform-shorthand-properties": "^7.16.7", + "@babel/plugin-transform-spread": "^7.16.7", + "@babel/plugin-transform-sticky-regex": "^7.16.7", + "@babel/plugin-transform-template-literals": "^7.16.7", + "@babel/plugin-transform-typeof-symbol": "^7.16.7", + "@babel/plugin-transform-unicode-escapes": "^7.16.7", + "@babel/plugin-transform-unicode-regex": "^7.16.7", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.16.8", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "core-js-compat": "^3.20.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", + "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.3", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.3", + "@babel/types": "^7.17.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", + "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", + "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", + "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@microsoft/signalr": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-6.0.3.tgz", + "integrity": "sha512-wWGVC2xi8OxNjyir8iQWuyxWHy3Dkakk2Q3VreCE7pDzFAgZ4pId6abJlRPMVIQxkUvUGc8knMW5l3sv2bJ/yw==", + "dependencies": { + "abort-controller": "^3.0.0", + "eventsource": "^1.0.7", + "fetch-cookie": "^0.11.0", + "node-fetch": "^2.6.1", + "ws": "^7.4.5" + } + }, + "node_modules/@ngtools/webpack": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.0.tgz", + "integrity": "sha512-QbTQWXK2WzYU+aKKVDG0ya7WYT+6rNAUXVt5ov9Nz1SGgDeozpiOx8ZqPWUvnToTY8EoodwWFGCVtkLHXUR+wA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^13.0.0", + "typescript": ">=4.4.3 <4.7", + "webpack": "^5.30.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/git": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz", + "integrity": "sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^1.3.2", + "lru-cache": "^6.0.0", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^6.1.1", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", + "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==", + "dev": true, + "dependencies": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "installed-package-contents": "index.js" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "dev": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz", + "integrity": "sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA==", + "dev": true + }, + "node_modules/@npmcli/promise-spawn": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz", + "integrity": "sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg==", + "dev": true, + "dependencies": { + "infer-owner": "^1.0.4" + } + }, + "node_modules/@npmcli/run-script": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-2.0.0.tgz", + "integrity": "sha512-fSan/Pu11xS/TdaTpTB0MRn9guwGU8dye+x56mEVgBEd/QsybBbYcAL0phPXi8SGWFEChkQd6M9qL4y6VOpFig==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^1.0.2", + "@npmcli/promise-spawn": "^1.3.2", + "node-gyp": "^8.2.0", + "read-package-json-fast": "^2.0.1" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz", + "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@schematics/angular": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.3.0.tgz", + "integrity": "sha512-WND6DXWf0ZFefqlC2hUm1FzHDonRfGpDEPWVhVulhYkB7IUUaXuCz8K41HAScyJ3bxUngs2Lx9+4omikc05fxA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "13.3.0", + "@angular-devkit/schematics": "13.3.0", + "jsonc-parser": "3.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.15.0 || >=16.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==", + "dev": true + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", + "dev": true + }, + "node_modules/@types/eslint": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", + "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/http-proxy": { + "version": "1.17.8", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", + "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.6.tgz", + "integrity": "sha512-hpQHs+lmZ0uuCrGyqypdI1Ho7jRFolOBT6OkNdZPFziLSSEKvWu+VxWU6bGdNEA/hoV4jV8pdDeNx8EWlmfNAw==", + "dev": true + }, + "node_modules/@types/jasminewd2": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.10.tgz", + "integrity": "sha512-J7mDz7ovjwjc+Y9rR9rY53hFWKATcIkrr9DwQWmOas4/pnIPJTXawnzjwpHm3RSxz/e3ZVUvQ7cRbd5UQLo10g==", + "dev": true, + "dependencies": { + "@types/jasmine": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "12.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.7.tgz", + "integrity": "sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA==", + "dev": true + }, + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "node_modules/@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "optional": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "dev": true + }, + "node_modules/@types/selenium-webdriver": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.19.tgz", + "integrity": "sha512-OFUilxQg+rWL2FMxtmIgCkUDlJB6pskkpvmew7yeXfzzsOBb5rc+y2+DjHm+r3r1ZPPcJefK3DveNSYWGiy68g==", + "optional": true + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "optional": true, + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz", + "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "devOptional": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apexcharts": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.34.0.tgz", + "integrity": "sha512-0HMwkTRm4lwuM4TZ+BjFynXuIRQnCG8E36k2r0JoxEfh61rY6OmRa6iFHlTQiyILpemPyTHxuPvK4wkR8RKc9A==", + "dependencies": { + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, + "node_modules/app-root-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", + "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==", + "dev": true, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "dependencies": { + "default-require-extensions": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz", + "integrity": "sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "optional": true + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/argparse/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "devOptional": true + }, + "node_modules/aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "dependencies": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "optional": true, + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "optional": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "node_modules/async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "optional": true + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.4.tgz", + "integrity": "sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.20.2", + "caniuse-lite": "^1.0.30001317", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "optional": true + }, + "node_modules/axobject-query": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", + "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", + "dev": true, + "dependencies": { + "ast-types-flow": "0.0.7" + } + }, + "node_modules/babel-loader": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz", + "integrity": "sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "engines": { + "node": ">= 8.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "webpack": ">=2" + } + }, + "node_modules/babel-loader/node_modules/json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/babel-loader/node_modules/loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "dependencies": { + "object.assign": "^4.1.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", + "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.3.1", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", + "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.1", + "core-js-compat": "^3.21.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", + "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "optional": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "blocking-proxy": "built/lib/bin.js" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "node_modules/bootstrap": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz", + "integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "peerDependencies": { + "jquery": "1.9.1 - 3", + "popper.js": "^1.16.1" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.20.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", + "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001317", + "electron-to-chromium": "^1.4.84", + "escalade": "^3.1.1", + "node-releases": "^2.0.2", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/browserstack": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.6.1.tgz", + "integrity": "sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==", + "optional": true, + "dependencies": { + "https-proxy-agent": "^2.2.1" + } + }, + "node_modules/browserstack/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "optional": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/browserstack/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/browserstack/node_modules/https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "optional": true, + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true + }, + "node_modules/buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001322", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001322.tgz", + "integrity": "sha512-neRmrmIrCGuMnxGSoh+x7zYtQFFgnSY2jaomjU56sCkTA6JINqQrxutF459JpWcWRajvoyn95sOXq4Pqrnyjew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "devOptional": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/circular-dependency-plugin": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz", + "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "webpack": ">=4.0.1" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/codelyzer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.2.tgz", + "integrity": "sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g==", + "dev": true, + "dependencies": { + "@angular/compiler": "9.0.0", + "@angular/core": "9.0.0", + "app-root-path": "^3.0.0", + "aria-query": "^3.0.0", + "axobject-query": "2.0.2", + "css-selector-tokenizer": "^0.7.1", + "cssauron": "^1.4.0", + "damerau-levenshtein": "^1.0.4", + "rxjs": "^6.5.3", + "semver-dsl": "^1.0.1", + "source-map": "^0.5.7", + "sprintf-js": "^1.1.2", + "tslib": "^1.10.0", + "zone.js": "~0.10.3" + }, + "peerDependencies": { + "@angular/compiler": ">=2.3.1 <13.0.0 || ^12.0.0-next || ^12.1.0-next || ^12.2.0-next", + "@angular/core": ">=2.3.1 <13.0.0 || ^12.0.0-next || ^12.1.0-next || ^12.2.0-next", + "tslint": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/codelyzer/node_modules/@angular/compiler": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.0.tgz", + "integrity": "sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==", + "dev": true, + "peerDependencies": { + "tslib": "^1.10.0" + } + }, + "node_modules/codelyzer/node_modules/@angular/core": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.0.tgz", + "integrity": "sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==", + "dev": true, + "peerDependencies": { + "rxjs": "^6.5.3", + "tslib": "^1.10.0", + "zone.js": "~0.10.2" + } + }, + "node_modules/codelyzer/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/codelyzer/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/codelyzer/node_modules/zone.js": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.3.tgz", + "integrity": "sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg==", + "dev": true + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "devOptional": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "devOptional": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, + "node_modules/colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "devOptional": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "node_modules/compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "devOptional": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-disposition/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.1.tgz", + "integrity": "sha512-nr81NhCAIpAWXGCK5thrKmfCQ6GDY0L5RN0U+BnIn/7Us55+UCex5ANNsNKmIVtDRnk0Ecf+/kzp9SUVrrBMLg==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^12.0.2", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.20.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/core-js": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", + "integrity": "sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", + "integrity": "sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==", + "dev": true, + "dependencies": { + "browserslist": "^4.19.1", + "semver": "7.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "devOptional": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-blank-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "bin": { + "css-has-pseudo": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-loader": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.5.1.tgz", + "integrity": "sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "dev": true, + "bin": { + "css-prefers-color-scheme": "dist/cli.cjs" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/css-what": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.0.1.tgz", + "integrity": "sha512-z93ZGFLNc6yaoXAmVhqoSIb+BduplteCt1fepvwhBUQK6MNE4g6fgjpuZKJKp0esUe+vXWlIkwZZjNWoOKw0ZA==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "dependencies": { + "through": "X.X.X" + } + }, + "node_modules/cssdb": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-5.1.0.tgz", + "integrity": "sha512-/vqjXhv1x9eGkE/zO6o8ZOI7dgdZbLVLUGyVRbPgk6YipXbW87YzUCcO+Jrmi5bwJlAH6oD+MNeZyRgXea1GZw==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "node_modules/daemon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/daemon/-/daemon-1.1.0.tgz", + "integrity": "sha1-bFECyB2wvoVvyQCPwsk1s5iGSug=", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/date-format": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.6.tgz", + "integrity": "sha512-B9vvg5rHuQ8cbUXE/RMWMyX2YA5TecT3jKF5fLtGNlzPlU7zblSPmAm2OImDbWL+LDOQ6pUm+4LOFz+ywS41Zw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "dependencies": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "dependencies": { + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "dependencies": { + "object-keys": "^1.0.12" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "optional": true, + "dependencies": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "optional": true, + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "optional": true, + "dependencies": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/del/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "optional": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "node_modules/dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "dev": true, + "dependencies": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "dependencies": { + "buffer-indexof": "^1.0.0" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domino": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", + "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.99", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.99.tgz", + "integrity": "sha512-YXMzbvlo6pW12KWw0bj6cIGCJi1Moy8PLCuuzgRzg6WYIcHILK3szU+HHnHFx2b373qRv+cfmHhbmRbatyAbPA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", + "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "dev": true, + "dependencies": { + "@socket.io/base64-arraybuffer": "~1.0.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", + "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "optional": true + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "optional": true, + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/esbuild": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.22.tgz", + "integrity": "sha512-CjFCFGgYtbFOPrwZNJf7wsuzesx8kqwAffOlbYcFDLFuUtP8xloK1GH+Ai13Qr0RZQf9tE7LMTHJ2iVGJ1SKZA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "esbuild-android-arm64": "0.14.22", + "esbuild-darwin-64": "0.14.22", + "esbuild-darwin-arm64": "0.14.22", + "esbuild-freebsd-64": "0.14.22", + "esbuild-freebsd-arm64": "0.14.22", + "esbuild-linux-32": "0.14.22", + "esbuild-linux-64": "0.14.22", + "esbuild-linux-arm": "0.14.22", + "esbuild-linux-arm64": "0.14.22", + "esbuild-linux-mips64le": "0.14.22", + "esbuild-linux-ppc64le": "0.14.22", + "esbuild-linux-riscv64": "0.14.22", + "esbuild-linux-s390x": "0.14.22", + "esbuild-netbsd-64": "0.14.22", + "esbuild-openbsd-64": "0.14.22", + "esbuild-sunos-64": "0.14.22", + "esbuild-windows-32": "0.14.22", + "esbuild-windows-64": "0.14.22", + "esbuild-windows-arm64": "0.14.22" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.22.tgz", + "integrity": "sha512-k1Uu4uC4UOFgrnTj2zuj75EswFSEBK+H6lT70/DdS4mTAOfs2ECv2I9ZYvr3w0WL0T4YItzJdK7fPNxcPw6YmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.22.tgz", + "integrity": "sha512-d8Ceuo6Vw6HM3fW218FB6jTY6O3r2WNcTAU0SGsBkXZ3k8SDoRLd3Nrc//EqzdgYnzDNMNtrWegK2Qsss4THhw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.22.tgz", + "integrity": "sha512-YAt9Tj3SkIUkswuzHxkaNlT9+sg0xvzDvE75LlBo4DI++ogSgSmKNR6B4eUhU5EUUepVXcXdRIdqMq9ppeRqfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.22.tgz", + "integrity": "sha512-ek1HUv7fkXMy87Qm2G4IRohN+Qux4IcnrDBPZGXNN33KAL0pEJJzdTv0hB/42+DCYWylSrSKxk3KUXfqXOoH4A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.22.tgz", + "integrity": "sha512-zPh9SzjRvr9FwsouNYTqgqFlsMIW07O8mNXulGeQx6O5ApgGUBZBgtzSlBQXkHi18WjrosYfsvp5nzOKiWzkjQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.22.tgz", + "integrity": "sha512-SnpveoE4nzjb9t2hqCIzzTWBM0RzcCINDMBB67H6OXIuDa4KqFqaIgmTchNA9pJKOVLVIKd5FYxNiJStli21qg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.22.tgz", + "integrity": "sha512-Zcl9Wg7gKhOWWNqAjygyqzB+fJa19glgl2JG7GtuxHyL1uEnWlpSMytTLMqtfbmRykIHdab797IOZeKwk5g0zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.22.tgz", + "integrity": "sha512-soPDdbpt/C0XvOOK45p4EFt8HbH5g+0uHs5nUKjHVExfgR7du734kEkXR/mE5zmjrlymk5AA79I0VIvj90WZ4g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.22.tgz", + "integrity": "sha512-8q/FRBJtV5IHnQChO3LHh/Jf7KLrxJ/RCTGdBvlVZhBde+dk3/qS9fFsUy+rs3dEi49aAsyVitTwlKw1SUFm+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.22.tgz", + "integrity": "sha512-SiNDfuRXhGh1JQLLA9JPprBgPVFOsGuQ0yDfSPTNxztmVJd8W2mX++c4FfLpAwxuJe183mLuKf7qKCHQs5ZnBQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.22.tgz", + "integrity": "sha512-6t/GI9I+3o1EFm2AyN9+TsjdgWCpg2nwniEhjm2qJWtJyJ5VzTXGUU3alCO3evopu8G0hN2Bu1Jhz2YmZD0kng==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.22.tgz", + "integrity": "sha512-AyJHipZKe88sc+tp5layovquw5cvz45QXw5SaDgAq2M911wLHiCvDtf/07oDx8eweCyzYzG5Y39Ih568amMTCQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.22.tgz", + "integrity": "sha512-Sz1NjZewTIXSblQDZWEFZYjOK6p8tV6hrshYdXZ0NHTjWE+lwxpOpWeElUGtEmiPcMT71FiuA9ODplqzzSxkzw==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.22.tgz", + "integrity": "sha512-TBbCtx+k32xydImsHxvFgsOCuFqCTGIxhzRNbgSL1Z2CKhzxwT92kQMhxort9N/fZM2CkRCPPs5wzQSamtzEHA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.22.tgz", + "integrity": "sha512-vK912As725haT313ANZZZN+0EysEEQXWC/+YE4rQvOQzLuxAQc2tjbzlAFREx3C8+uMuZj/q7E5gyVB7TzpcTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.22.tgz", + "integrity": "sha512-/mbJdXTW7MTcsPhtfDsDyPEOju9EOABvCjeUU2OJ7fWpX/Em/H3WYDa86tzLUbcVg++BScQDzqV/7RYw5XNY0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.14.22.tgz", + "integrity": "sha512-FOSAM29GN1fWusw0oLMv6JYhoheDIh5+atC72TkJKfIUMID6yISlicoQSd9gsNSFsNBvABvtE2jR4JB1j4FkFw==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.22.tgz", + "integrity": "sha512-1vRIkuvPTjeSVK3diVrnMLSbkuE36jxA+8zGLUOrT4bb7E/JZvDRhvtbWXWaveUc/7LbhaNFhHNvfPuSw2QOQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.22.tgz", + "integrity": "sha512-AxjIDcOmx17vr31C5hp20HIwz1MymtMjKqX4qL6whPj0dT9lwxPexmLj6G1CpR3vFhui6m75EnBEe4QL82SYqw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.22.tgz", + "integrity": "sha512-5wvQ+39tHmRhNpu2Fx04l7QfeK3mQ9tKzDqqGR8n/4WUxsFxnVLfDRBGirIfk4AfWlxk60kqirlODPoT5LqMUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "devOptional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/eventsource": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", + "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "dependencies": { + "original": "^1.0.0" + }, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "optional": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/express/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "engines": [ + "node >=0.6.0" + ], + "optional": true + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "devOptional": true + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fetch-cookie": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz", + "integrity": "sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==", + "dependencies": { + "tough-cookie": "^2.3.3 || ^3.0.1 || ^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "dependencies": { + "glob": "^7.0.3", + "minimatch": "^3.0.3" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "devOptional": true + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "devOptional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", + "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", + "dev": true, + "dependencies": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "optional": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/har-validator/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/har-validator/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "optional": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "devOptional": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "devOptional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz", + "integrity": "sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz", + "integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz", + "integrity": "sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "optional": true + }, + "node_modules/immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/inquirer": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz", + "integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.2.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/inquirer/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/inquirer/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/inquirer/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/rxjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", + "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", + "dev": true, + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/inquirer/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "devOptional": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "optional": true, + "dependencies": { + "is-path-inside": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "optional": true, + "dependencies": { + "path-is-inside": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "optional": true + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "devOptional": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true + }, + "node_modules/istanbul-api": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.7.tgz", + "integrity": "sha512-LYTOa2UrYFyJ/aSczZi/6lBykVMjCCvUmT64gOe+jPZFy4w6FYfPGqFT2IiQ2BxVHHDOvCD7qrIXb0EOh4uGWw==", + "dev": true, + "dependencies": { + "async": "^2.6.2", + "compare-versions": "^3.4.0", + "fileset": "^2.0.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.5", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "minimatch": "^3.0.4", + "once": "^1.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-api/node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-api/node_modules/istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "dependencies": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-api/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-api/node_modules/make-dir/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/istanbul-api/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-api/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "dependencies": { + "append-transform": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz", + "integrity": "sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report/node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "optional": true, + "dependencies": { + "exit": "^0.1.2", + "glob": "^7.0.6", + "jasmine-core": "~2.8.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", + "dev": true + }, + "node_modules/jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "dependencies": { + "colors": "1.1.2" + } + }, + "node_modules/jasmine/node_modules/jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "optional": true + }, + "node_modules/jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "optional": true, + "engines": { + "node": ">= 6.9.x" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "devOptional": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "devOptional": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "optional": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true + }, + "node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "optional": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jszip": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", + "optional": true, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, + "node_modules/karma": { + "version": "6.3.17", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.17.tgz", + "integrity": "sha512-2TfjHwrRExC8yHoWlPBULyaLwAFmXmxQrcuFImt/JsAsSZu1uOWTZ1ZsWjqQtWpHLiatJOHL5jFjXSJIgCd01g==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.2.0", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-coverage-istanbul-reporter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.1.1.tgz", + "integrity": "sha512-CH8lTi8+kKXGvrhy94+EkEMldLCiUA0xMOiL31vvli9qK0T+qcXJAwWBRVJWnVWxYkTmyWar8lPz63dxX6/z1A==", + "dev": true, + "dependencies": { + "istanbul-api": "^2.1.6", + "minimatch": "^3.0.4" + }, + "funding": { + "url": "https://github.com/sponsors/mattlewis92" + } + }, + "node_modules/karma-jasmine": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", + "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==", + "dev": true, + "dependencies": { + "jasmine-core": "^3.3" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "karma": "*" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz", + "integrity": "sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ==", + "dev": true, + "peerDependencies": { + "jasmine-core": ">=3.8", + "karma": ">=0.9", + "karma-jasmine": ">=1.1" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/less": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz", + "integrity": "sha512-EoQp/Et7OSOVu0aJknJOtlXZsnr8XE8KwuzTHOLeVSEx8pVWUICc8Q0VYRHgzyjX78nMEyC/oztWFbgyhtNfDA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^2.5.2", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-10.2.0.tgz", + "integrity": "sha512-AV5KHWvCezW27GT90WATaDnfXBv99llDbtaj4bshq6DvAihMdNjaPDcUMa6EXKLRF+P2opFenJp89BXg91XLYg==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "optional": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz", + "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.4.4.tgz", + "integrity": "sha512-ncaWPsuw9Vl1CKA406hVnJLGQKy1OHx6buk8J4rE2lVW+NW5Y82G5/DIloO7NkqLOUtNPEANaWC1kZYVjXssPw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.6", + "debug": "^4.3.4", + "flatted": "^3.2.5", + "rfdc": "^1.3.0", + "streamroller": "^3.0.6" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/log4js/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "dependencies": { + "sourcemap-codec": "^1.4.4" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "optional": true + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", + "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", + "dev": true, + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz", + "integrity": "sha512-YseMB8cs8U/KCaAGQoqYmfUuhhGW0a9p9XvWXrxVOkE3/IiISTLw4ALNt7JR5B2eYauFM+PQGSbXMDmVbR7Tfw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "devOptional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "devOptional": true + }, + "node_modules/minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "node_modules/multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "dependencies": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", + "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "dev": true, + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/ngx-toastr": { + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-14.2.2.tgz", + "integrity": "sha512-/Ajr9E0llr51Zij8WgnxQpe7a5JK+k1n07/uWJcQ112OBH0GCktHi8M8QfGvw5Ih67hG8iowrT+aHXHS49gZcQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0-0", + "@angular/core": ">=12.0.0-0", + "@angular/platform-browser": ">=12.0.0-0" + } + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-forge": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", + "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/node-releases": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", + "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "dev": true + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "node_modules/npm-package-arg": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz", + "integrity": "sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "semver": "^7.3.4", + "validate-npm-package-name": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-packlist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz", + "integrity": "sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==", + "dev": true, + "dependencies": { + "glob": "^7.1.6", + "ignore-walk": "^4.0.1", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-pick-manifest": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz", + "integrity": "sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==", + "dev": true, + "dependencies": { + "npm-install-checks": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1", + "npm-package-arg": "^8.1.2", + "semver": "^7.3.4" + } + }, + "node_modules/npm-registry-fetch": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-12.0.2.tgz", + "integrity": "sha512-Df5QT3RaJnXYuOwtXBXS9BWs+tHH2olvkCLh6jcR/b/u3DvPMlp3J0TvvYwplPKxHMOwfg287PYih9QqaVFoKA==", + "dev": true, + "dependencies": { + "make-fetch-happen": "^10.0.1", + "minipass": "^3.1.6", + "minipass-fetch": "^1.4.1", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^8.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/npm-registry-fetch/node_modules/@npmcli/fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.0.tgz", + "integrity": "sha512-DmfBvNXGaetMxj9LTp8NAN9vEidXURrf5ZTslQzEAi/6GbW+4yjaLFQc6Tue5cpZ9Frlk4OBo/Snf1Bh/S7qTQ==", + "dev": true, + "dependencies": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm-registry-fetch/node_modules/cacache": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.0.3.tgz", + "integrity": "sha512-eC7wYodNCVb97kuHGk5P+xZsvUJHkhSEOyNwkenqQPAsOtrTjvWOE5vSPNBpz9d8X3acIf6w2Ub5s4rvOCTs4g==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^1.1.2", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^7.2.0", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.11", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/npm-registry-fetch/node_modules/lru-cache": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.7.1.tgz", + "integrity": "sha512-cRffBiTW8s73eH4aTXqBcTLU0xQnwGV3/imttRHGWCrbergmnK4D6JXQd8qin5z43HnDwRI+o7mVW0LEB+tpAw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.1.1.tgz", + "integrity": "sha512-3/mCljDQNjmrP7kl0vhS5WVlV+TvSKoZaFhdiYV7MOijEnrhrjaVnqbp/EY/7S+fhUB2KpH7j8c1iRsIOs+kjw==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.0.2", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.1.1", + "ssri": "^8.0.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/npm-registry-fetch/node_modules/make-fetch-happen/node_modules/minipass-fetch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.0.tgz", + "integrity": "sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==", + "dev": true, + "dependencies": { + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.1.tgz", + "integrity": "sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg==", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.0", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/oidc-client": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.11.5.tgz", + "integrity": "sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==", + "dependencies": { + "acorn": "^7.4.1", + "base64-js": "^1.5.1", + "core-js": "^3.8.3", + "crypto-js": "^4.0.0", + "serialize-javascript": "^4.0.0" + } + }, + "node_modules/oidc-client/node_modules/serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "dependencies": { + "url-parse": "^1.4.3" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", + "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", + "dev": true, + "dependencies": { + "@types/retry": "^0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "devOptional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-12.0.3.tgz", + "integrity": "sha512-CdYEl03JDrRO3x18uHjBYA9TyoW8gy+ThVcypcDkxPtKlw76e4ejhYB6i9lJ+/cebbjpqPW/CijjqxwDTts8Ow==", + "dev": true, + "dependencies": { + "@npmcli/git": "^2.1.0", + "@npmcli/installed-package-contents": "^1.0.6", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^2.0.0", + "cacache": "^15.0.5", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.3", + "mkdirp": "^1.0.3", + "npm-package-arg": "^8.0.1", + "npm-packlist": "^3.0.0", + "npm-pick-manifest": "^6.0.0", + "npm-registry-fetch": "^12.0.0", + "promise-retry": "^2.0.1", + "read-package-json-fast": "^2.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.0" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "devOptional": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz", + "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1", + "parse5-sax-parser": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz", + "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==", + "dev": true, + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "devOptional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "optional": true + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "devOptional": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "optional": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "optional": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "dependencies": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "engines": { + "node": ">= 0.12.0" + } + }, + "node_modules/portfinder/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/portfinder/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/postcss": { + "version": "8.4.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", + "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", + "dev": true, + "dependencies": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz", + "integrity": "sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.2" + }, + "peerDependencies": { + "postcss": "^8.0.2" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.2.tgz", + "integrity": "sha512-DXVtwUhIk4f49KK5EGuEdgx4Gnyj6+t2jBSEmxvpIK9QI40tWrpS2Pua8Q7iIZWBrki2QOaeUdEaLPPa91K0RQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.3.tgz", + "integrity": "sha512-fESawWJCrBV035DcbKRPAVmy21LpoyiXdPTuHUfWJ14ZRjY7Y7PA6P4g8z6LQGYhU1WAxkTxjIjurXzoe68Glw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz", + "integrity": "sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.3" + } + }, + "node_modules/postcss-custom-media": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz", + "integrity": "sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-custom-properties": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz", + "integrity": "sha512-FHbbB/hRo/7cxLGkc2NS7cDRIDN1oFqQnUKBiyh4b/gwk8DD8udvmRDpUhEK836kB8ggUCieHVOvZDnF9XhI3g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz", + "integrity": "sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.2" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.4.tgz", + "integrity": "sha512-I8epwGy5ftdzNWEYok9VjW9whC4xnelAtbajGv4adql4FIF09rnrxnA9Y8xSHN47y7gqFIv10C5+ImsLeJpKBw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.1.tgz", + "integrity": "sha512-jM+CGkTs4FcG53sMPjrrGE0rIvLDdCrqMzgDC5fLI7JHDO7o6QG8C5TQBtExb13hdBdoH9C2QVbG4jo2y9lErQ==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz", + "integrity": "sha512-rPPZRLPmEKgLk/KlXMqRaNkYTUpE7YC+bOIQFN5xcu1Vp11Y4faIXv6/Jpft6FMnl6YRxZqDZG0qQOW80stzxQ==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.6.tgz", + "integrity": "sha512-KfdC6vg53GC+vPd2+HYzsZ6obmPqOk6HY09kttU19+Gj1nC3S3XBVEXDHxkhxTohgZqzbUb94bKXvKDnYWBm/A==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-import": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", + "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-lab-function": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.1.2.tgz", + "integrity": "sha512-isudf5ldhg4fk16M8viAwAbg6Gv14lVO35N3Z/49NhbwPQ2xbiEoHgrRgpgQojosF4vF7jY653ktB6dDrUOR8Q==", + "dev": true, + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-nesting": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.1.3.tgz", + "integrity": "sha512-wUC+/YCik4wH3StsbC5fBG1s2Z3ZV74vjGqBFYtmYKlVxoio5TYGM06AiaKkQPPlkXWn72HKfS7Cw5PYxnoXSw==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz", + "integrity": "sha512-CxZwoWup9KXzQeeIxtgOciQ00tDtnylYIlJBBODqkgS/PU2jISuWOL/mYLHmZb9ZhZiCaNKsCRiLp22dZUtNsg==", + "dev": true, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.4.tgz", + "integrity": "sha512-MrgKeiiu5OC/TETQO45kV3npRjOFxEHthsqGtkh3I1rPbZSbXGD/lZVi9j13cYh+NA8PIAPyk6sGjT9QbRyvSg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.2.3.tgz", + "integrity": "sha512-Ok0DhLfwrcNGrBn8sNdy1uZqWRk/9FId0GiQ39W4ILop5GHtjJs8bu1MY9isPwHInpVEPWjb4CEcEaSbBLpfwA==", + "dev": true, + "dependencies": { + "autoprefixer": "^10.4.2", + "browserslist": "^4.19.1", + "caniuse-lite": "^1.0.30001299", + "css-blank-pseudo": "^3.0.2", + "css-has-pseudo": "^3.0.3", + "css-prefers-color-scheme": "^6.0.2", + "cssdb": "^5.0.0", + "postcss-attribute-case-insensitive": "^5.0.0", + "postcss-color-functional-notation": "^4.2.1", + "postcss-color-hex-alpha": "^8.0.2", + "postcss-color-rebeccapurple": "^7.0.2", + "postcss-custom-media": "^8.0.0", + "postcss-custom-properties": "^12.1.2", + "postcss-custom-selectors": "^6.0.0", + "postcss-dir-pseudo-class": "^6.0.3", + "postcss-double-position-gradients": "^3.0.4", + "postcss-env-function": "^4.0.4", + "postcss-focus-visible": "^6.0.3", + "postcss-focus-within": "^5.0.3", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.2", + "postcss-image-set-function": "^4.0.4", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.0.3", + "postcss-logical": "^5.0.3", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.1.2", + "postcss-overflow-shorthand": "^3.0.2", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.3", + "postcss-pseudo-class-any-link": "^7.0.2", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^5.0.0" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.1.tgz", + "integrity": "sha512-JRoLFvPEX/1YTPxRxp1JO4WxBVXJYrSY7NHeak5LImwJ+VobFMwYDQHvfTXEpcn+7fYIeGkC29zYFhFWIZD8fg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.9" + }, + "engines": { + "node": "^12 || ^14 || >=16" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz", + "integrity": "sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz", + "integrity": "sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "devOptional": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/protractor": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz", + "integrity": "sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==", + "optional": true, + "dependencies": { + "@types/q": "^0.0.32", + "@types/selenium-webdriver": "^3.0.0", + "blocking-proxy": "^1.0.0", + "browserstack": "^1.5.1", + "chalk": "^1.1.3", + "glob": "^7.0.3", + "jasmine": "2.8.0", + "jasminewd2": "^2.1.0", + "q": "1.4.1", + "saucelabs": "^1.5.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "~0.4.0", + "webdriver-js-extender": "2.1.0", + "webdriver-manager": "^12.1.7", + "yargs": "^15.3.1" + }, + "bin": { + "protractor": "bin/protractor", + "webdriver-manager": "bin/webdriver-manager" + }, + "engines": { + "node": ">=10.13.x" + } + }, + "node_modules/protractor/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "optional": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "optional": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/protractor/node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/protractor/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "node_modules/protractor/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "optional": true, + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/protractor/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/protractor/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "optional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/protractor/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "optional": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/protractor/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "optional": true + }, + "node_modules/protractor/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "optional": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/protractor/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "optional": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true, + "optional": true + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "optional": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "dev": true, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz", + "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", + "integrity": "sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", + "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "optional": true, + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/request/node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "optional": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "optional": true, + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "optional": true + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "node_modules/resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "devOptional": true, + "dependencies": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/rxjs/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "node_modules/sass": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.0.tgz", + "integrity": "sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/sass-loader": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.4.0.tgz", + "integrity": "sha512-7xN+8khDIzym1oL9XyS6zP6Ges+Bo2B2xbPrjdMHEYyV3AQYhd/wXeru++3ODHF0zMjYmVadblSKrPrjEkL8mg==", + "dev": true, + "dependencies": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "sass": "^1.3.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/saucelabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", + "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "optional": true, + "dependencies": { + "https-proxy-agent": "^2.2.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/saucelabs/node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "optional": true, + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/saucelabs/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/saucelabs/node_modules/https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "optional": true, + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "devOptional": true + }, + "node_modules/schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 8.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "node_modules/selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "optional": true, + "dependencies": { + "jszip": "^3.1.3", + "rimraf": "^2.5.4", + "tmp": "0.0.30", + "xml2js": "^0.4.17" + }, + "engines": { + "node": ">= 6.9.0" + } + }, + "node_modules/selenium-webdriver/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/selenium-webdriver/node_modules/tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "optional": true, + "dependencies": { + "os-tmpdir": "~1.0.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/selfsigned": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", + "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", + "dev": true, + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "dependencies": { + "semver": "^5.3.0" + } + }, + "node_modules/semver-dsl/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/service": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/service/-/service-0.1.4.tgz", + "integrity": "sha1-0Kuf+8K51Yda+LAd7DYzl4RSW0Q=", + "dependencies": { + "daemon": ">=0.3.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "devOptional": true + }, + "node_modules/set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", + "dev": true + }, + "node_modules/socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "dev": true, + "dependencies": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", + "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "dev": true, + "dependencies": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", + "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz", + "integrity": "sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==", + "dev": true, + "dependencies": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "devOptional": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + }, + "node_modules/sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "optional": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.0.6.tgz", + "integrity": "sha512-Qz32plKq/MZywYyhEatxyYc8vs994Gz0Hu2MSYXXLD233UyPeIeRBZARIIGwFer4Mdb8r3Y2UqKkgyDghM6QCg==", + "dev": true, + "dependencies": { + "date-format": "^4.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.0.1" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/stylus": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.56.0.tgz", + "integrity": "sha512-Ev3fOb4bUElwWu4F9P9WjnnaSpc8XB9OFHSFZSKMFL1CE1oM+oFXWEgAqPmmZIyhBihuqIQlFsVTypiiS9RxeA==", + "dev": true, + "dependencies": { + "css": "^3.0.0", + "debug": "^4.3.2", + "glob": "^7.1.6", + "safer-buffer": "^2.1.2", + "sax": "~1.2.4", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stylus-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-6.2.0.tgz", + "integrity": "sha512-5dsDc7qVQGRoc6pvCL20eYgRUxepZ9FpeK28XhdXaIPP6kXr6nI1zAAKFQgP5OBkOfKaURp4WUpJzspg1f01Gg==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.7", + "klona": "^2.0.4", + "normalize-path": "^3.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "stylus": ">=0.52.4", + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "devOptional": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "devOptional": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/terser": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.11.0.tgz", + "integrity": "sha512-uCA9DLanzzWSsN1UirKwylhhRz3aKPInlfmpGfw8VN6jHsAtu8HJtIpeeHHK23rxnE/cDc+yvmq5wqkIC6Kn0A==", + "dev": true, + "dependencies": { + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", + "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "dev": true, + "dependencies": { + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1", + "terser": "^5.7.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser-webpack-plugin/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toastr": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz", + "integrity": "sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=", + "dependencies": { + "jquery": ">=1.12.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.4.1.tgz", + "integrity": "sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==", + "optional": true, + "dependencies": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + }, + "bin": { + "ts-node": "dist/bin.js" + }, + "engines": { + "node": ">=4.2.0" + }, + "peerDependencies": { + "typescript": ">=2.0" + } + }, + "node_modules/tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "node_modules/tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "devOptional": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev" + } + }, + "node_modules/tslint/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "devOptional": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/tslint/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "devOptional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tslint/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "devOptional": true + }, + "node_modules/tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "devOptional": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "devOptional": true + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", + "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "devOptional": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "devOptional": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "dependencies": { + "builtins": "^1.0.3" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "engines": [ + "node >=0.6.0" + ], + "optional": true, + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", + "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webdriver-js-extender": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz", + "integrity": "sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==", + "optional": true, + "dependencies": { + "@types/selenium-webdriver": "^3.0.0", + "selenium-webdriver": "^3.0.1" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/webdriver-manager": { + "version": "12.1.8", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz", + "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==", + "optional": true, + "dependencies": { + "adm-zip": "^0.4.9", + "chalk": "^1.1.1", + "del": "^2.2.0", + "glob": "^7.0.3", + "ini": "^1.3.4", + "minimist": "^1.2.0", + "q": "^1.4.1", + "request": "^2.87.0", + "rimraf": "^2.5.2", + "semver": "^5.3.0", + "xml2js": "^0.4.17" + }, + "bin": { + "webdriver-manager": "bin/webdriver-manager" + }, + "engines": { + "node": ">=6.9.x" + } + }, + "node_modules/webdriver-manager/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webdriver-manager/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webdriver-manager/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "optional": true, + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webdriver-manager/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "optional": true + }, + "node_modules/webdriver-manager/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/webdriver-manager/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/webdriver-manager/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webdriver-manager/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "optional": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/webpack": { + "version": "5.70.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", + "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.4.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.9.2", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.3.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.0.tgz", + "integrity": "sha512-MouJz+rXAm9B1OTOYaJnn6rtD/lWZPy2ufQCH3BPs8Rloh/Du6Jze4p7AeLYHkVi0giJnYLaSGDC7S+GM9arhg==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.2.2", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz", + "integrity": "sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/serve-index": "^1.9.1", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.2.2", + "ansi-html-community": "^0.0.8", + "bonjour": "^3.5.0", + "chokidar": "^3.5.2", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "default-gateway": "^6.0.3", + "del": "^6.0.0", + "express": "^4.17.1", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.0", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "portfinder": "^1.0.28", + "schema-utils": "^4.0.0", + "selfsigned": "^2.0.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "spdy": "^4.0.2", + "strip-ansi": "^7.0.0", + "webpack-dev-middleware": "^5.3.0", + "ws": "^8.1.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/webpack-dev-server/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-dev-server/node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-server/node_modules/strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/webpack/node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "optional": true + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "devOptional": true + }, + "node_modules/ws": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", + "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "optional": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "optional": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", + "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/zone.js": { + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.5.tgz", + "integrity": "sha512-D1/7VxEuQ7xk6z/kAROe4SUbd9CzxY4zOwVGnGHerd/SgLIVU5f4esDzQUsOCeArn933BZfWMKydH7l7dPEp0g==", + "dependencies": { + "tslib": "^2.3.0" + } + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-1.1.1.tgz", + "integrity": "sha512-YVAcA4DKLOj296CF5SrQ8cYiMRiUGc2sqFpLxsDGWE34suHqhGP/5yMsDHKsrh8hs8I5TiRVXNwKPWQpX3iGjw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "sourcemap-codec": "1.4.8" + } + }, + "@angular-devkit/architect": { + "version": "0.1303.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1303.0.tgz", + "integrity": "sha512-kTcKB917ICA8j53SGo4gn+qAlzx8si+iHnOTbp5QlMr7qt/Iz07SVVI8mRlMD6c6lr7eE/fVlCLzEZ1+WCQpTA==", + "dev": true, + "requires": { + "@angular-devkit/core": "13.3.0", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/build-angular": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-13.3.0.tgz", + "integrity": "sha512-3Ji7EeqGHj7i1Jgmeo3aIEXsnfKyFeQPpl65gcYmHwj5dP4lZzLSU4rMaWWUKksccgqCUXgPI2vKePTPazmikg==", + "dev": true, + "requires": { + "@ampproject/remapping": "1.1.1", + "@angular-devkit/architect": "0.1303.0", + "@angular-devkit/build-webpack": "0.1303.0", + "@angular-devkit/core": "13.3.0", + "@babel/core": "7.16.12", + "@babel/generator": "7.16.8", + "@babel/helper-annotate-as-pure": "7.16.7", + "@babel/plugin-proposal-async-generator-functions": "7.16.8", + "@babel/plugin-transform-async-to-generator": "7.16.8", + "@babel/plugin-transform-runtime": "7.16.10", + "@babel/preset-env": "7.16.11", + "@babel/runtime": "7.16.7", + "@babel/template": "7.16.7", + "@discoveryjs/json-ext": "0.5.6", + "@ngtools/webpack": "13.3.0", + "ansi-colors": "4.1.1", + "babel-loader": "8.2.3", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.9.1", + "cacache": "15.3.0", + "circular-dependency-plugin": "5.2.2", + "copy-webpack-plugin": "10.2.1", + "core-js": "3.20.3", + "critters": "0.0.16", + "css-loader": "6.5.1", + "esbuild": "0.14.22", + "esbuild-wasm": "0.14.22", + "glob": "7.2.0", + "https-proxy-agent": "5.0.0", + "inquirer": "8.2.0", + "jsonc-parser": "3.0.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.2", + "less-loader": "10.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.0", + "mini-css-extract-plugin": "2.5.3", + "minimatch": "3.0.4", + "open": "8.4.0", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "6.0.1", + "piscina": "3.2.0", + "postcss": "8.4.5", + "postcss-import": "14.0.2", + "postcss-loader": "6.2.1", + "postcss-preset-env": "7.2.3", + "regenerator-runtime": "0.13.9", + "resolve-url-loader": "5.0.0", + "rxjs": "6.6.7", + "sass": "1.49.0", + "sass-loader": "12.4.0", + "semver": "7.3.5", + "source-map-loader": "3.0.1", + "source-map-support": "0.5.21", + "stylus": "0.56.0", + "stylus-loader": "6.2.0", + "terser": "5.11.0", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.3.1", + "webpack": "5.70.0", + "webpack-dev-middleware": "5.3.0", + "webpack-dev-server": "4.7.3", + "webpack-merge": "5.8.0", + "webpack-subresource-integrity": "5.1.0" + }, + "dependencies": { + "core-js": { + "version": "3.20.3", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz", + "integrity": "sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag==", + "dev": true + } + } + }, + "@angular-devkit/build-webpack": { + "version": "0.1303.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1303.0.tgz", + "integrity": "sha512-a+Veg2oYn3RM2Kl148BReuONmD1kjbbYBnMUVi8nD6rvJPStFZkqN5s5ZkYybKeWnzMGaB3VasKR88z5XeH22A==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1303.0", + "rxjs": "6.6.7" + } + }, + "@angular-devkit/core": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-13.3.0.tgz", + "integrity": "sha512-8YrreVbWlJVZnk5zs4vfkRItrPEtWhUcxWOBfYT/Kwu4FwJVAnNuhJAxxXOAQ2Ckd7cv30Idh/RFVLbTZ5Gs9w==", + "dev": true, + "requires": { + "ajv": "8.9.0", + "ajv-formats": "2.1.1", + "fast-json-stable-stringify": "2.1.0", + "magic-string": "0.25.7", + "rxjs": "6.6.7", + "source-map": "0.7.3" + } + }, + "@angular-devkit/schematics": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-13.3.0.tgz", + "integrity": "sha512-hq7tqnB3uVT/iDgqWWZ4kvnijeAcgd4cfLzZiCPaYn1nuhZf0tWsho6exhJ/odMZHvVp7w8OibqWiUKxNY9zHA==", + "dev": true, + "requires": { + "@angular-devkit/core": "13.3.0", + "jsonc-parser": "3.0.0", + "magic-string": "0.25.7", + "ora": "5.4.1", + "rxjs": "6.6.7" + } + }, + "@angular/animations": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-13.3.0.tgz", + "integrity": "sha512-q7hkImhHCv0QdriR8HOFhsAW05QDmvapcHrBv3y862LUTR5e90/+81RYuwFuKX1lk/sa7LiHlHHWC7oCspzr2Q==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/cli": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.0.tgz", + "integrity": "sha512-2qCKP/QsyxrJnpd3g4P/iTQ4TjI04N8r+bG5YLLfudoMDsQ/Ti4ogdI7PBeG2IMbRylZW9XLjHraWG42+Y9tWw==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1303.0", + "@angular-devkit/core": "13.3.0", + "@angular-devkit/schematics": "13.3.0", + "@schematics/angular": "13.3.0", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.1", + "debug": "4.3.3", + "ini": "2.0.0", + "inquirer": "8.2.0", + "jsonc-parser": "3.0.0", + "npm-package-arg": "8.1.5", + "npm-pick-manifest": "6.1.1", + "open": "8.4.0", + "ora": "5.4.1", + "pacote": "12.0.3", + "resolve": "1.22.0", + "semver": "7.3.5", + "symbol-observable": "4.0.0", + "uuid": "8.3.2" + } + }, + "@angular/common": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-13.3.0.tgz", + "integrity": "sha512-yl09TWBmz++Z3MKjzZIwU2wZHiedCn1DjGILjjNXegHFOfINRHiqLhHca4kGWFcTsdvcuEhd9Hk9JATqi45rjg==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/compiler": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-13.3.0.tgz", + "integrity": "sha512-oeUvaBOVpey2G1I5fWZa3JcyRuBQ3dAeRay5UtQhu1Xw2L8jd2tYkbZb1XOgP9J1/Ma4LO62pjSaOpR2EtO5ww==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/compiler-cli": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-13.3.0.tgz", + "integrity": "sha512-f9m55YejHJNIDTwHyGwf3wn5AvZepDfdAgeJP0Re4XmO1mf/Z9Ob5mJP5Q1yLNhqk0DlURWsZ1CbJqufPXMTbQ==", + "dev": true, + "requires": { + "@babel/core": "^7.17.2", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "dependency-graph": "^0.11.0", + "magic-string": "^0.26.0", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "sourcemap-codec": "^1.4.8", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.1.2.tgz", + "integrity": "sha512-hoyByceqwKirw7w3Z7gnIIZC3Wx3J484Y3L/cMpXFbr7d9ZQj2mODrirNzcJa+SM3UlpWXYvKV4RlRpFXlWgXg==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.0" + } + }, + "@babel/core": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.17.8.tgz", + "integrity": "sha512-OdQDV/7cRBtJHLSOBqqbYNkOcydOgnX59TZx4puf41fzcVtN3e/4yqY8lMQsK+5X2lJtAdmA+6OHqsj1hBJ4IQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.7", + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helpers": "^7.17.8", + "@babel/parser": "^7.17.8", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "magic-string": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.26.1.tgz", + "integrity": "sha512-ndThHmvgtieXe8J/VGPjG+Apu7v7ItcD5mhEIvOscWjPF/ccOiLxHaSuCAS2G+3x4GKsAbT8u7zdyamupui8Tg==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.8" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@angular/core": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-13.3.0.tgz", + "integrity": "sha512-ZnuIMEK8YFBtthNqrxapYolMp6qRy4Yp/VG+M11YNiuBp/BoYYDjTaknwO8vu36Cn6372zWjcibsknkZMjdBkg==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/forms": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-13.3.0.tgz", + "integrity": "sha512-eBySo+B3/AV+p3SmD15Tg41N+SoxYPyqGnlCTR+jSrFis5ZZNWf0kKpIKhJhW2taRq6K+1o3KcA0W9bnphrZDQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/language-service": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-13.3.0.tgz", + "integrity": "sha512-XzfamYk39h+ASxb2ycZKfn9nITmns+jTV+DPrFAY1BoW+4x3tAoqt3/CkdJn8UaCePswKXckDQ6y9i109TfddQ==", + "dev": true + }, + "@angular/platform-browser": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-13.3.0.tgz", + "integrity": "sha512-OgNVgRtqTPxzItZbJVe4NmSYKDLEKQYjGulStWl4ycQTsOKteF+sJi8gU5BvEU/KQNZItYnIQxMqTsFyS7xlRQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-13.3.0.tgz", + "integrity": "sha512-7/r79Yn8SDH8t0/fJ26PmScm/S1JZ9hxjC8IoROdyC5xBrSGrp946mIKE/4/813zmF8uPj2lveV9p/XiKTbxSw==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/platform-server": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-13.3.0.tgz", + "integrity": "sha512-kfKyFi77f4MfcVfsv2O9icYz8FdqTjFdU1C8YDSC0OTgy4/QmwQ3lboZwcsvMRfF/aAb3liiFhPvNAjC2CYSEw==", + "requires": { + "domino": "^2.1.2", + "tslib": "^2.3.0", + "xhr2": "^0.2.0" + } + }, + "@angular/router": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-13.3.0.tgz", + "integrity": "sha512-Kz657mtycup+s9emRH66etkBobAF26h3UDXE9pnjUM6MuVTA38P31WyTWKyWJVk8Oruxm/hTHZZBfI88o9/1sA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, + "@babel/code-frame": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", + "integrity": "sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg==", + "devOptional": true, + "requires": { + "@babel/highlight": "^7.16.7" + } + }, + "@babel/compat-data": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.17.7.tgz", + "integrity": "sha512-p8pdE6j0a29TNGebNm7NzYZWB3xVZJBZ7XGs42uAKzQo8VQ3F0By/cQCtUEABwIqw5zo6WA4NbmxsfzADzMKnQ==", + "dev": true + }, + "@babel/core": { + "version": "7.16.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz", + "integrity": "sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.16.8", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helpers": "^7.16.7", + "@babel/parser": "^7.16.12", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.10", + "@babel/types": "^7.16.8", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz", + "integrity": "sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.8", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz", + "integrity": "sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz", + "integrity": "sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA==", + "dev": true, + "requires": { + "@babel/helper-explode-assignable-expression": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.17.7.tgz", + "integrity": "sha512-UFzlz2jjd8kroj0hmCFV5zr+tQPi1dpC2cRsDV/3IEW8bJfCPrPpmcSN6ZS8RqIq4LXcmpipCQFPddyFA5Yc7w==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-validator-option": "^7.16.7", + "browserslist": "^4.17.5", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz", + "integrity": "sha512-SogLLSxXm2OkBbSsHZMM4tUi8fUzjs63AT/d0YQIzr6GSd8Hxsbk2KYDX0k0DweAzGMj/YWeiCsorIdtdcW8Eg==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-member-expression-to-functions": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7" + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.17.0.tgz", + "integrity": "sha512-awO2So99wG6KnlE+TPs6rn83gCz5WlEePJDTnLEqbchMVrBeAujURVphRdigsk094VhvZehFoNOihSlcBjwsXA==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "regexpu-core": "^5.0.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz", + "integrity": "sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", + "integrity": "sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-explode-assignable-expression": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz", + "integrity": "sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-function-name": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz", + "integrity": "sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz", + "integrity": "sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz", + "integrity": "sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.17.7.tgz", + "integrity": "sha512-thxXgnQ8qQ11W2wVUObIqDL4p148VMxkt5T/qpN5k2fboRyzFGFmKsTGViquyM5QHKUy48OZoca8kw4ajaDPyw==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-module-transforms": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.17.7.tgz", + "integrity": "sha512-VmZD99F3gNTYB7fJRDTi+u6l/zxY0BE6OIxPSU7a50s6ZUQkHwSDmV92FfM+oCG0pZRVojGYhkR8I0OGeCVREw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz", + "integrity": "sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz", + "integrity": "sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-wrap-function": "^7.16.8", + "@babel/types": "^7.16.8" + } + }, + "@babel/helper-replace-supers": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz", + "integrity": "sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-member-expression-to-functions": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/traverse": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-simple-access": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.17.7.tgz", + "integrity": "sha512-txyMCGroZ96i+Pxr3Je3lzEJjqwaRC9buMUgtomcrLe5Nd0+fk1h0LLA+ixUF5OW7AhHuQ7Es1WcQJZmZsz2XA==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz", + "integrity": "sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz", + "integrity": "sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw==", + "dev": true, + "requires": { + "@babel/types": "^7.16.7" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", + "devOptional": true + }, + "@babel/helper-validator-option": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz", + "integrity": "sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ==", + "dev": true + }, + "@babel/helper-wrap-function": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz", + "integrity": "sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw==", + "dev": true, + "requires": { + "@babel/helper-function-name": "^7.16.7", + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.16.8", + "@babel/types": "^7.16.8" + } + }, + "@babel/helpers": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.17.8.tgz", + "integrity": "sha512-QcL86FGxpfSJwGtAvv4iG93UL6bmqBdmoVY0CMCU2g+oD2ezQse3PT5Pa+jiD6LJndBQi0EDlpzOWNlLuhz5gw==", + "dev": true, + "requires": { + "@babel/template": "^7.16.7", + "@babel/traverse": "^7.17.3", + "@babel/types": "^7.17.0" + } + }, + "@babel/highlight": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.10.tgz", + "integrity": "sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw==", + "devOptional": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz", + "integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ==", + "dev": true + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz", + "integrity": "sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz", + "integrity": "sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-proposal-optional-chaining": "^7.16.7" + } + }, + "@babel/plugin-proposal-async-generator-functions": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz", + "integrity": "sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-remap-async-to-generator": "^7.16.8", + "@babel/plugin-syntax-async-generators": "^7.8.4" + } + }, + "@babel/plugin-proposal-class-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz", + "integrity": "sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-proposal-class-static-block": { + "version": "7.17.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.17.6.tgz", + "integrity": "sha512-X/tididvL2zbs7jZCeeRJ8167U/+Ac135AM6jCAx6gYXDUviZV5Ku9UDvWS2NCuWlFjIRXklYhwo6HhAC7ETnA==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.17.6", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + } + }, + "@babel/plugin-proposal-dynamic-import": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz", + "integrity": "sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + } + }, + "@babel/plugin-proposal-export-namespace-from": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz", + "integrity": "sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + } + }, + "@babel/plugin-proposal-json-strings": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz", + "integrity": "sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-json-strings": "^7.8.3" + } + }, + "@babel/plugin-proposal-logical-assignment-operators": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz", + "integrity": "sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + } + }, + "@babel/plugin-proposal-nullish-coalescing-operator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz", + "integrity": "sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + } + }, + "@babel/plugin-proposal-numeric-separator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz", + "integrity": "sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + } + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.17.3.tgz", + "integrity": "sha512-yuL5iQA/TbZn+RGAfxQXfi7CNLmKi1f8zInn4IgobuCWcAb7i+zj4TYzQ9l8cEzVyJ89PDGuqxK1xZpUDISesw==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.17.0", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.16.7" + } + }, + "@babel/plugin-proposal-optional-catch-binding": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz", + "integrity": "sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + } + }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz", + "integrity": "sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "@babel/plugin-proposal-private-methods": { + "version": "7.16.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz", + "integrity": "sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw==", + "dev": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.16.10", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz", + "integrity": "sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-create-class-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + } + }, + "@babel/plugin-proposal-unicode-property-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz", + "integrity": "sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz", + "integrity": "sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz", + "integrity": "sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-remap-async-to-generator": "^7.16.8" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz", + "integrity": "sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz", + "integrity": "sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz", + "integrity": "sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ==", + "dev": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.16.7", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-optimise-call-expression": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz", + "integrity": "sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.17.7.tgz", + "integrity": "sha512-XVh0r5yq9sLR4vZ6eVZe8FKfIcSgaTBxVBRSYokRj2qksf6QerYnTxz9/GTuKTH/n/HwLP7t6gtlybHetJ/6hQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz", + "integrity": "sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz", + "integrity": "sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz", + "integrity": "sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA==", + "dev": true, + "requires": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz", + "integrity": "sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz", + "integrity": "sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA==", + "dev": true, + "requires": { + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz", + "integrity": "sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz", + "integrity": "sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz", + "integrity": "sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.17.7.tgz", + "integrity": "sha512-ITPmR2V7MqioMJyrxUo2onHNC3e+MvfFiFIR0RP21d3PtlVb6sfzoxNKiphSZUOM9hEIdzCcZe83ieX3yoqjUA==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-simple-access": "^7.17.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.17.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.17.8.tgz", + "integrity": "sha512-39reIkMTUVagzgA5x88zDYXPCMT6lcaRKs1+S9K6NKBPErbgO/w/kP8GlNQTC87b412ZTlmNgr3k2JrWgHH+Bw==", + "dev": true, + "requires": { + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-module-transforms": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-identifier": "^7.16.7", + "babel-plugin-dynamic-import-node": "^2.3.3" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz", + "integrity": "sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ==", + "dev": true, + "requires": { + "@babel/helper-module-transforms": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.16.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz", + "integrity": "sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz", + "integrity": "sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz", + "integrity": "sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-replace-supers": "^7.16.7" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz", + "integrity": "sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz", + "integrity": "sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz", + "integrity": "sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q==", + "dev": true, + "requires": { + "regenerator-transform": "^0.14.2" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz", + "integrity": "sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.16.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.10.tgz", + "integrity": "sha512-9nwTiqETv2G7xI4RvXHNfpGdr8pAA+Q/YtN3yLK7OoK7n9OibVm/xymJ838a9A6E/IciOLPj82lZk0fW6O4O7w==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", + "integrity": "sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz", + "integrity": "sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.16.0" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz", + "integrity": "sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz", + "integrity": "sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz", + "integrity": "sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz", + "integrity": "sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz", + "integrity": "sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q==", + "dev": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7" + } + }, + "@babel/preset-env": { + "version": "7.16.11", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.11.tgz", + "integrity": "sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.16.8", + "@babel/helper-compilation-targets": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", + "@babel/helper-validator-option": "^7.16.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.16.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.16.7", + "@babel/plugin-proposal-async-generator-functions": "^7.16.8", + "@babel/plugin-proposal-class-properties": "^7.16.7", + "@babel/plugin-proposal-class-static-block": "^7.16.7", + "@babel/plugin-proposal-dynamic-import": "^7.16.7", + "@babel/plugin-proposal-export-namespace-from": "^7.16.7", + "@babel/plugin-proposal-json-strings": "^7.16.7", + "@babel/plugin-proposal-logical-assignment-operators": "^7.16.7", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", + "@babel/plugin-proposal-numeric-separator": "^7.16.7", + "@babel/plugin-proposal-object-rest-spread": "^7.16.7", + "@babel/plugin-proposal-optional-catch-binding": "^7.16.7", + "@babel/plugin-proposal-optional-chaining": "^7.16.7", + "@babel/plugin-proposal-private-methods": "^7.16.11", + "@babel/plugin-proposal-private-property-in-object": "^7.16.7", + "@babel/plugin-proposal-unicode-property-regex": "^7.16.7", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-transform-arrow-functions": "^7.16.7", + "@babel/plugin-transform-async-to-generator": "^7.16.8", + "@babel/plugin-transform-block-scoped-functions": "^7.16.7", + "@babel/plugin-transform-block-scoping": "^7.16.7", + "@babel/plugin-transform-classes": "^7.16.7", + "@babel/plugin-transform-computed-properties": "^7.16.7", + "@babel/plugin-transform-destructuring": "^7.16.7", + "@babel/plugin-transform-dotall-regex": "^7.16.7", + "@babel/plugin-transform-duplicate-keys": "^7.16.7", + "@babel/plugin-transform-exponentiation-operator": "^7.16.7", + "@babel/plugin-transform-for-of": "^7.16.7", + "@babel/plugin-transform-function-name": "^7.16.7", + "@babel/plugin-transform-literals": "^7.16.7", + "@babel/plugin-transform-member-expression-literals": "^7.16.7", + "@babel/plugin-transform-modules-amd": "^7.16.7", + "@babel/plugin-transform-modules-commonjs": "^7.16.8", + "@babel/plugin-transform-modules-systemjs": "^7.16.7", + "@babel/plugin-transform-modules-umd": "^7.16.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.16.8", + "@babel/plugin-transform-new-target": "^7.16.7", + "@babel/plugin-transform-object-super": "^7.16.7", + "@babel/plugin-transform-parameters": "^7.16.7", + "@babel/plugin-transform-property-literals": "^7.16.7", + "@babel/plugin-transform-regenerator": "^7.16.7", + "@babel/plugin-transform-reserved-words": "^7.16.7", + "@babel/plugin-transform-shorthand-properties": "^7.16.7", + "@babel/plugin-transform-spread": "^7.16.7", + "@babel/plugin-transform-sticky-regex": "^7.16.7", + "@babel/plugin-transform-template-literals": "^7.16.7", + "@babel/plugin-transform-typeof-symbol": "^7.16.7", + "@babel/plugin-transform-unicode-escapes": "^7.16.7", + "@babel/plugin-transform-unicode-regex": "^7.16.7", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.16.8", + "babel-plugin-polyfill-corejs2": "^0.3.0", + "babel-plugin-polyfill-corejs3": "^0.5.0", + "babel-plugin-polyfill-regenerator": "^0.3.0", + "core-js-compat": "^3.20.2", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", + "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", + "dev": true, + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@babel/template": { + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz", + "integrity": "sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/parser": "^7.16.7", + "@babel/types": "^7.16.7" + } + }, + "@babel/traverse": { + "version": "7.17.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.17.3.tgz", + "integrity": "sha512-5irClVky7TxRWIRtxlh2WPUUOLhcPN06AGgaQSB8AEwuyEBgJVuJ5imdHm5zxk8w0QS5T+tDfnDxAlhWjpb7cw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.7", + "@babel/generator": "^7.17.3", + "@babel/helper-environment-visitor": "^7.16.7", + "@babel/helper-function-name": "^7.16.7", + "@babel/helper-hoist-variables": "^7.16.7", + "@babel/helper-split-export-declaration": "^7.16.7", + "@babel/parser": "^7.17.3", + "@babel/types": "^7.17.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "@babel/generator": { + "version": "7.17.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.7.tgz", + "integrity": "sha512-oLcVCTeIFadUoArDTwpluncplrYBmTCCZZgXCbgNGvOBBiSDDK3eWO4b/+eOTli5tKv1lg+a5/NAXg+nTcei1w==", + "dev": true, + "requires": { + "@babel/types": "^7.17.0", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz", + "integrity": "sha512-TmKSNO4D5rzhL5bjWFcVHHLETzfQ/AmbKpKPOSjlP0WoHZ6L911fgoOKY4Alp/emzG4cHJdyN49zpgkbXFEHHw==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.16.7", + "to-fast-properties": "^2.0.0" + } + }, + "@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true + }, + "@csstools/postcss-progressive-custom-properties": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-1.3.0.tgz", + "integrity": "sha512-ASA9W1aIy5ygskZYuWams4BzafD12ULvSypmaLJT2jvQ8G0M3I8PRQhC0h7mG0Z3LI05+agZjqSR9+K9yaQQjA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "@discoveryjs/json-ext": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", + "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", + "dev": true + }, + "@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/resolve-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz", + "integrity": "sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", + "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.4.tgz", + "integrity": "sha512-vFv9ttIedivx0ux3QSjhgtCVjPZd5l46ZOMDSCwnH1yUO2e964gO8LZGyv2QkqcgR6TnBU1v+1IFqmeoG+0UJQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@microsoft/signalr": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@microsoft/signalr/-/signalr-6.0.3.tgz", + "integrity": "sha512-wWGVC2xi8OxNjyir8iQWuyxWHy3Dkakk2Q3VreCE7pDzFAgZ4pId6abJlRPMVIQxkUvUGc8knMW5l3sv2bJ/yw==", + "requires": { + "abort-controller": "^3.0.0", + "eventsource": "^1.0.7", + "fetch-cookie": "^0.11.0", + "node-fetch": "^2.6.1", + "ws": "^7.4.5" + } + }, + "@ngtools/webpack": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.3.0.tgz", + "integrity": "sha512-QbTQWXK2WzYU+aKKVDG0ya7WYT+6rNAUXVt5ov9Nz1SGgDeozpiOx8ZqPWUvnToTY8EoodwWFGCVtkLHXUR+wA==", + "dev": true, + "requires": {} + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, + "requires": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "@npmcli/git": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz", + "integrity": "sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw==", + "dev": true, + "requires": { + "@npmcli/promise-spawn": "^1.3.2", + "lru-cache": "^6.0.0", + "mkdirp": "^1.0.4", + "npm-pick-manifest": "^6.1.1", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "@npmcli/installed-package-contents": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz", + "integrity": "sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw==", + "dev": true, + "requires": { + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "@npmcli/node-gyp": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz", + "integrity": "sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA==", + "dev": true + }, + "@npmcli/promise-spawn": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz", + "integrity": "sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg==", + "dev": true, + "requires": { + "infer-owner": "^1.0.4" + } + }, + "@npmcli/run-script": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-2.0.0.tgz", + "integrity": "sha512-fSan/Pu11xS/TdaTpTB0MRn9guwGU8dye+x56mEVgBEd/QsybBbYcAL0phPXi8SGWFEChkQd6M9qL4y6VOpFig==", + "dev": true, + "requires": { + "@npmcli/node-gyp": "^1.0.2", + "@npmcli/promise-spawn": "^1.3.2", + "node-gyp": "^8.2.0", + "read-package-json-fast": "^2.0.1" + } + }, + "@popperjs/core": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz", + "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==" + }, + "@schematics/angular": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-13.3.0.tgz", + "integrity": "sha512-WND6DXWf0ZFefqlC2hUm1FzHDonRfGpDEPWVhVulhYkB7IUUaXuCz8K41HAScyJ3bxUngs2Lx9+4omikc05fxA==", + "dev": true, + "requires": { + "@angular-devkit/core": "13.3.0", + "@angular-devkit/schematics": "13.3.0", + "jsonc-parser": "3.0.0" + } + }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==", + "dev": true + }, + "@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true + }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==", + "dev": true + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", + "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", + "dev": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==", + "dev": true + }, + "@types/eslint": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.4.1.tgz", + "integrity": "sha512-GE44+DNEyxxh2Kc6ro/VkIj+9ma0pO0bwv9+uHSyBrikYOHr8zYcdPvnBOp1aw8s+CjRvuSx7CyWqRrNFQ59mA==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.3.tgz", + "integrity": "sha512-PB3ldyrcnAicT35TWPs5IcwKD8S333HMaa2VVv4+wdvebJkjWuW/xESoB8IwRcog8HYVYamb1g/R31Qv5Bx03g==", + "dev": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "0.0.51", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", + "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "dev": true + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.28", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", + "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/http-proxy": { + "version": "1.17.8", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz", + "integrity": "sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/jasmine": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-3.4.6.tgz", + "integrity": "sha512-hpQHs+lmZ0uuCrGyqypdI1Ho7jRFolOBT6OkNdZPFziLSSEKvWu+VxWU6bGdNEA/hoV4jV8pdDeNx8EWlmfNAw==", + "dev": true + }, + "@types/jasminewd2": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/jasminewd2/-/jasminewd2-2.0.10.tgz", + "integrity": "sha512-J7mDz7ovjwjc+Y9rR9rY53hFWKATcIkrr9DwQWmOas4/pnIPJTXawnzjwpHm3RSxz/e3ZVUvQ7cRbd5UQLo10g==", + "dev": true, + "requires": { + "@types/jasmine": "*" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "@types/node": { + "version": "12.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.7.tgz", + "integrity": "sha512-JNbGaHFCLwgHn/iCckiGSOZ1XYHsKFwREtzPwSGCVld1SGhOlmZw2D4ZI94HQCrBHbADzW9m4LER/8olJTRGHA==", + "dev": true + }, + "@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true + }, + "@types/q": { + "version": "0.0.32", + "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", + "integrity": "sha1-vShOV8hPEyXacCur/IKlMoGQwMU=", + "optional": true + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "@types/retry": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz", + "integrity": "sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g==", + "dev": true + }, + "@types/selenium-webdriver": { + "version": "3.0.19", + "resolved": "https://registry.npmjs.org/@types/selenium-webdriver/-/selenium-webdriver-3.0.19.tgz", + "integrity": "sha512-OFUilxQg+rWL2FMxtmIgCkUDlJB6pskkpvmew7yeXfzzsOBb5rc+y2+DjHm+r3r1ZPPcJefK3DveNSYWGiy68g==", + "optional": true + }, + "@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@webassemblyjs/ast": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", + "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "dev": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", + "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "dev": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", + "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "dev": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", + "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "dev": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", + "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "dev": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", + "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "dev": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", + "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", + "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "dev": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", + "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "dev": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", + "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "dev": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", + "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/helper-wasm-section": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-opt": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "@webassemblyjs/wast-printer": "1.11.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", + "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", + "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-buffer": "1.11.1", + "@webassemblyjs/wasm-gen": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", + "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/helper-wasm-bytecode": "1.11.1", + "@webassemblyjs/ieee754": "1.11.1", + "@webassemblyjs/leb128": "1.11.1", + "@webassemblyjs/utf8": "1.11.1" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", + "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "dev": true, + "requires": { + "@webassemblyjs/ast": "1.11.1", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "abab": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", + "dev": true + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + }, + "adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==", + "optional": true + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "agentkeepalive": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", + "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "depd": "^1.1.2", + "humanize-ms": "^1.2.1" + } + }, + "aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "requires": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + } + }, + "ajv": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz", + "integrity": "sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "devOptional": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "devOptional": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "apexcharts": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.34.0.tgz", + "integrity": "sha512-0HMwkTRm4lwuM4TZ+BjFynXuIRQnCG8E36k2r0JoxEfh61rY6OmRa6iFHlTQiyILpemPyTHxuPvK4wkR8RKc9A==", + "requires": { + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, + "app-root-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz", + "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==", + "dev": true + }, + "append-transform": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", + "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", + "dev": true, + "requires": { + "default-require-extensions": "^2.0.0" + } + }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "are-we-there-yet": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.0.tgz", + "integrity": "sha512-0GWpv50YSOcLXaN6/FAKY3vfRbllXWV2xvfA/oKJF8pzFhWXPV+yjhJXDBbjscDYowv7Yw1A3uigpzn5iEGTyw==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "optional": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "devOptional": true, + "requires": { + "sprintf-js": "~1.0.2" + }, + "dependencies": { + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "devOptional": true + } + } + }, + "aria-query": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-3.0.0.tgz", + "integrity": "sha1-ZbP8wcoRVajJrmTW7uKX8V1RM8w=", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7", + "commander": "^2.11.0" + } + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "optional": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "optional": true + }, + "asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "optional": true, + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "optional": true + }, + "ast-types-flow": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", + "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", + "dev": true + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "dev": true, + "requires": { + "lodash": "^4.17.14" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "optional": true + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true + }, + "autoprefixer": { + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.4.tgz", + "integrity": "sha512-Tm8JxsB286VweiZ5F0anmbyGiNI3v3wGv3mz9W+cxEDYB/6jbnj6GM9H9mK3wIL8ftgl+C07Lcwb8PG5PCCPzA==", + "dev": true, + "requires": { + "browserslist": "^4.20.2", + "caniuse-lite": "^1.0.30001317", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + } + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "optional": true + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", + "optional": true + }, + "axobject-query": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", + "integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", + "dev": true, + "requires": { + "ast-types-flow": "0.0.7" + } + }, + "babel-loader": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz", + "integrity": "sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw==", + "dev": true, + "requires": { + "find-cache-dir": "^3.3.1", + "loader-utils": "^1.4.0", + "make-dir": "^3.1.0", + "schema-utils": "^2.6.5" + }, + "dependencies": { + "json5": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", + "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "loader-utils": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", + "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" + } + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "dev": true, + "requires": { + "object.assign": "^4.1.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz", + "integrity": "sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.11", + "@babel/helper-define-polyfill-provider": "^0.3.1", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", + "integrity": "sha512-G3uJih0XWiID451fpeFaYGVuxHEjzKTHtc9uGFEjR6hHrvNzeS/PX+LLLcetJcytsB5m4j+K3o/EpXJNb/5IEQ==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.1", + "core-js-compat": "^3.21.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz", + "integrity": "sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A==", + "dev": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.3.1" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "devOptional": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "blocking-proxy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/blocking-proxy/-/blocking-proxy-1.0.1.tgz", + "integrity": "sha512-KE8NFMZr3mN2E0HcvCgRtX7DjhiIQrwle+nSVJVC/yqFb9+xznHl2ZcoBp2L9qzkI4t4cBFJ1efXF8Dwi132RA==", + "optional": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.9.7", + "raw-body": "2.4.3", + "type-is": "~1.6.18" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "bonjour": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", + "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", + "dev": true, + "requires": { + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "bootstrap": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.1.tgz", + "integrity": "sha512-0dj+VgI9Ecom+rvvpNZ4MUZJz8dcX7WCX+eTID9+/8HgOkv3dsRzi8BGeZJCQU6flWQVYxwTQnEZFrmJSEO7og==", + "requires": {} + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "devOptional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browserslist": { + "version": "4.20.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.20.2.tgz", + "integrity": "sha512-CQOBCqp/9pDvDbx3xfMi+86pr4KXIf2FDkTTdeuYw8OxS9t898LA1Khq57gtufFILXpfgsSx5woNgsBgvGjpsA==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001317", + "electron-to-chromium": "^1.4.84", + "escalade": "^3.1.1", + "node-releases": "^2.0.2", + "picocolors": "^1.0.0" + } + }, + "browserstack": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/browserstack/-/browserstack-1.6.1.tgz", + "integrity": "sha512-GxtFjpIaKdbAyzHfFDKixKO8IBT7wR3NjbzrGc78nNs/Ciys9wU3/nBtsqsWv5nDSrdI5tz0peKuzCPuNXNUiw==", + "optional": true, + "requires": { + "https-proxy-agent": "^2.2.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "optional": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "optional": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + } + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true + }, + "buffer-indexof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", + "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "devOptional": true + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=", + "dev": true + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, + "cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, + "requires": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + } + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "devOptional": true + }, + "caniuse-lite": { + "version": "1.0.30001322", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001322.tgz", + "integrity": "sha512-neRmrmIrCGuMnxGSoh+x7zYtQFFgnSY2jaomjU56sCkTA6JINqQrxutF459JpWcWRajvoyn95sOXq4Pqrnyjew==", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "optional": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "devOptional": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true + }, + "circular-dependency-plugin": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/circular-dependency-plugin/-/circular-dependency-plugin-5.2.2.tgz", + "integrity": "sha512-g38K9Cm5WRwlaH6g03B9OEz/0qRizI+2I7n+Gz+L5DxXJAPAiWQvwlYNm1V1jkdpUv95bOe/ASm2vfi/G560jQ==", + "dev": true, + "requires": {} + }, + "clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + } + }, + "codelyzer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codelyzer/-/codelyzer-6.0.2.tgz", + "integrity": "sha512-v3+E0Ucu2xWJMOJ2fA/q9pDT/hlxHftHGPUay1/1cTgyPV5JTHFdO9hqo837Sx2s9vKBMTt5gO+lhF95PO6J+g==", + "dev": true, + "requires": { + "@angular/compiler": "9.0.0", + "@angular/core": "9.0.0", + "app-root-path": "^3.0.0", + "aria-query": "^3.0.0", + "axobject-query": "2.0.2", + "css-selector-tokenizer": "^0.7.1", + "cssauron": "^1.4.0", + "damerau-levenshtein": "^1.0.4", + "rxjs": "^6.5.3", + "semver-dsl": "^1.0.1", + "source-map": "^0.5.7", + "sprintf-js": "^1.1.2", + "tslib": "^1.10.0", + "zone.js": "~0.10.3" + }, + "dependencies": { + "@angular/compiler": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-9.0.0.tgz", + "integrity": "sha512-ctjwuntPfZZT2mNj2NDIVu51t9cvbhl/16epc5xEwyzyDt76pX9UgwvY+MbXrf/C/FWwdtmNtfP698BKI+9leQ==", + "dev": true, + "requires": {} + }, + "@angular/core": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-9.0.0.tgz", + "integrity": "sha512-6Pxgsrf0qF9iFFqmIcWmjJGkkCaCm6V5QNnxMy2KloO3SDq6QuMVRbN9RtC8Urmo25LP+eZ6ZgYqFYpdD8Hd9w==", + "dev": true, + "requires": {} + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "zone.js": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.10.3.tgz", + "integrity": "sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg==", + "dev": true + } + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "devOptional": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "devOptional": true + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true + }, + "colorette": { + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", + "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "optional": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "devOptional": true + }, + "commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", + "dev": true + }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, + "component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", + "dev": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "requires": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "dependencies": { + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "devOptional": true + }, + "connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "requires": { + "safe-buffer": "5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", + "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", + "dev": true + }, + "copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "requires": { + "is-what": "^3.14.1" + } + }, + "copy-webpack-plugin": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.1.tgz", + "integrity": "sha512-nr81NhCAIpAWXGCK5thrKmfCQ6GDY0L5RN0U+BnIn/7Us55+UCex5ANNsNKmIVtDRnk0Ecf+/kzp9SUVrrBMLg==", + "dev": true, + "requires": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^12.0.2", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "core-js": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.21.1.tgz", + "integrity": "sha512-FRq5b/VMrWlrmCzwRrpDYNxyHP9BcAZC+xHJaqTgIE5091ZV1NTmyh0sGOg5XqpnHvR0svdy0sv1gWA1zmhxig==" + }, + "core-js-compat": { + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.21.1.tgz", + "integrity": "sha512-gbgX5AUvMb8gwxC7FLVWYT7Kkgu/y7+h/h1X43yJkNqhlK2fuYyQimqvKGNZFAY6CKii/GFKJ2cp/1/42TN36g==", + "dev": true, + "requires": { + "browserslist": "^4.19.1", + "semver": "7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "devOptional": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", + "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", + "dev": true, + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } + }, + "critters": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.16.tgz", + "integrity": "sha512-JwjgmO6i3y6RWtLYmXwO5jMd+maZt8Tnfu7VVISmEWyQqfLpB8soBswf8/2bu6SBXxtKA68Al3c+qIG1ApT68A==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "css-select": "^4.2.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "postcss": "^8.3.7", + "pretty-bytes": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, + "css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "css-blank-pseudo": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz", + "integrity": "sha512-VS90XWtsHGqoM0t4KpH053c4ehxZ2E6HtGI7x68YFV0pTo/QmkV/YFA+NnlvK8guxZVNWGQhVNJGC39Q8XF4OQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-has-pseudo": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.4.tgz", + "integrity": "sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "css-loader": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.5.1.tgz", + "integrity": "sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ==", + "dev": true, + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.2.15", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.1.0", + "semver": "^7.3.5" + } + }, + "css-prefers-color-scheme": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.3.tgz", + "integrity": "sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==", + "dev": true, + "requires": {} + }, + "css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-selector-tokenizer": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.3.tgz", + "integrity": "sha512-jWQv3oCEL5kMErj4wRnK/OPoBi0D+P1FR2cDCKYPaMeD2eW3/mttav8HT4hT1CKopiJI/psEULjkClhvJo4Lvg==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "css-what": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.0.1.tgz", + "integrity": "sha512-z93ZGFLNc6yaoXAmVhqoSIb+BduplteCt1fepvwhBUQK6MNE4g6fgjpuZKJKp0esUe+vXWlIkwZZjNWoOKw0ZA==", + "dev": true + }, + "cssauron": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/cssauron/-/cssauron-1.4.0.tgz", + "integrity": "sha1-pmAt/34EqDBtwNuaVR6S6LVmKtg=", + "dev": true, + "requires": { + "through": "X.X.X" + } + }, + "cssdb": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-5.1.0.tgz", + "integrity": "sha512-/vqjXhv1x9eGkE/zO6o8ZOI7dgdZbLVLUGyVRbPgk6YipXbW87YzUCcO+Jrmi5bwJlAH6oD+MNeZyRgXea1GZw==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true + }, + "custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", + "dev": true + }, + "daemon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/daemon/-/daemon-1.1.0.tgz", + "integrity": "sha1-bFECyB2wvoVvyQCPwsk1s5iGSug=" + }, + "damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "date-format": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.6.tgz", + "integrity": "sha512-B9vvg5rHuQ8cbUXE/RMWMyX2YA5TecT3jKF5fLtGNlzPlU7zblSPmAm2OImDbWL+LDOQ6pUm+4LOFz+ywS41Zw==", + "dev": true + }, + "debug": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "optional": true + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "dev": true, + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, + "default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "requires": { + "execa": "^5.0.0" + } + }, + "default-require-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", + "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", + "dev": true, + "requires": { + "strip-bom": "^3.0.0" + } + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "dev": true, + "requires": { + "object-keys": "^1.0.12" + } + }, + "del": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", + "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", + "optional": true, + "requires": { + "globby": "^5.0.0", + "is-path-cwd": "^1.0.0", + "is-path-in-cwd": "^1.0.0", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "rimraf": "^2.2.8" + }, + "dependencies": { + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "optional": true, + "requires": { + "array-uniq": "^1.0.1" + } + }, + "globby": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", + "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", + "optional": true, + "requires": { + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", + "dev": true + }, + "dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", + "dev": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", + "dev": true + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "devOptional": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", + "dev": true + }, + "dns-packet": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "dev": true, + "requires": { + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" + } + }, + "dns-txt": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", + "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "dev": true, + "requires": { + "buffer-indexof": "^1.0.0" + } + }, + "dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", + "dev": true, + "requires": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "dev": true + }, + "domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domino": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", + "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==" + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "optional": true, + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", + "dev": true + }, + "electron-to-chromium": { + "version": "1.4.99", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.99.tgz", + "integrity": "sha512-YXMzbvlo6pW12KWw0bj6cIGCJi1Moy8PLCuuzgRzg6WYIcHILK3szU+HHnHFx2b373qRv+cfmHhbmRbatyAbPA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "devOptional": true + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", + "dev": true + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "engine.io": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", + "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", + "dev": true, + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" + }, + "dependencies": { + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "dev": true, + "requires": {} + } + } + }, + "engine.io-parser": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", + "dev": true, + "requires": { + "@socket.io/base64-arraybuffer": "~1.0.2" + } + }, + "enhanced-resolve": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.2.tgz", + "integrity": "sha512-GIm3fQfwLJ8YZx2smuHpBKkXC1yOk+OBEmKckVyL0i/ea8mqDEykK3ld5dgH1QYPNyT/lIllxV2LULnxCHaHkA==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", + "dev": true + }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-module-lexer": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", + "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "dev": true + }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "optional": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "optional": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, + "esbuild": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.22.tgz", + "integrity": "sha512-CjFCFGgYtbFOPrwZNJf7wsuzesx8kqwAffOlbYcFDLFuUtP8xloK1GH+Ai13Qr0RZQf9tE7LMTHJ2iVGJ1SKZA==", + "dev": true, + "optional": true, + "requires": { + "esbuild-android-arm64": "0.14.22", + "esbuild-darwin-64": "0.14.22", + "esbuild-darwin-arm64": "0.14.22", + "esbuild-freebsd-64": "0.14.22", + "esbuild-freebsd-arm64": "0.14.22", + "esbuild-linux-32": "0.14.22", + "esbuild-linux-64": "0.14.22", + "esbuild-linux-arm": "0.14.22", + "esbuild-linux-arm64": "0.14.22", + "esbuild-linux-mips64le": "0.14.22", + "esbuild-linux-ppc64le": "0.14.22", + "esbuild-linux-riscv64": "0.14.22", + "esbuild-linux-s390x": "0.14.22", + "esbuild-netbsd-64": "0.14.22", + "esbuild-openbsd-64": "0.14.22", + "esbuild-sunos-64": "0.14.22", + "esbuild-windows-32": "0.14.22", + "esbuild-windows-64": "0.14.22", + "esbuild-windows-arm64": "0.14.22" + } + }, + "esbuild-android-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.22.tgz", + "integrity": "sha512-k1Uu4uC4UOFgrnTj2zuj75EswFSEBK+H6lT70/DdS4mTAOfs2ECv2I9ZYvr3w0WL0T4YItzJdK7fPNxcPw6YmQ==", + "dev": true, + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.22.tgz", + "integrity": "sha512-d8Ceuo6Vw6HM3fW218FB6jTY6O3r2WNcTAU0SGsBkXZ3k8SDoRLd3Nrc//EqzdgYnzDNMNtrWegK2Qsss4THhw==", + "dev": true, + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.22.tgz", + "integrity": "sha512-YAt9Tj3SkIUkswuzHxkaNlT9+sg0xvzDvE75LlBo4DI++ogSgSmKNR6B4eUhU5EUUepVXcXdRIdqMq9ppeRqfw==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.22.tgz", + "integrity": "sha512-ek1HUv7fkXMy87Qm2G4IRohN+Qux4IcnrDBPZGXNN33KAL0pEJJzdTv0hB/42+DCYWylSrSKxk3KUXfqXOoH4A==", + "dev": true, + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.22.tgz", + "integrity": "sha512-zPh9SzjRvr9FwsouNYTqgqFlsMIW07O8mNXulGeQx6O5ApgGUBZBgtzSlBQXkHi18WjrosYfsvp5nzOKiWzkjQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-32": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.22.tgz", + "integrity": "sha512-SnpveoE4nzjb9t2hqCIzzTWBM0RzcCINDMBB67H6OXIuDa4KqFqaIgmTchNA9pJKOVLVIKd5FYxNiJStli21qg==", + "dev": true, + "optional": true + }, + "esbuild-linux-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.22.tgz", + "integrity": "sha512-Zcl9Wg7gKhOWWNqAjygyqzB+fJa19glgl2JG7GtuxHyL1uEnWlpSMytTLMqtfbmRykIHdab797IOZeKwk5g0zg==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.22.tgz", + "integrity": "sha512-soPDdbpt/C0XvOOK45p4EFt8HbH5g+0uHs5nUKjHVExfgR7du734kEkXR/mE5zmjrlymk5AA79I0VIvj90WZ4g==", + "dev": true, + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.22.tgz", + "integrity": "sha512-8q/FRBJtV5IHnQChO3LHh/Jf7KLrxJ/RCTGdBvlVZhBde+dk3/qS9fFsUy+rs3dEi49aAsyVitTwlKw1SUFm+A==", + "dev": true, + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.22.tgz", + "integrity": "sha512-SiNDfuRXhGh1JQLLA9JPprBgPVFOsGuQ0yDfSPTNxztmVJd8W2mX++c4FfLpAwxuJe183mLuKf7qKCHQs5ZnBQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.22.tgz", + "integrity": "sha512-6t/GI9I+3o1EFm2AyN9+TsjdgWCpg2nwniEhjm2qJWtJyJ5VzTXGUU3alCO3evopu8G0hN2Bu1Jhz2YmZD0kng==", + "dev": true, + "optional": true + }, + "esbuild-linux-riscv64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.22.tgz", + "integrity": "sha512-AyJHipZKe88sc+tp5layovquw5cvz45QXw5SaDgAq2M911wLHiCvDtf/07oDx8eweCyzYzG5Y39Ih568amMTCQ==", + "dev": true, + "optional": true + }, + "esbuild-linux-s390x": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.22.tgz", + "integrity": "sha512-Sz1NjZewTIXSblQDZWEFZYjOK6p8tV6hrshYdXZ0NHTjWE+lwxpOpWeElUGtEmiPcMT71FiuA9ODplqzzSxkzw==", + "dev": true, + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.22.tgz", + "integrity": "sha512-TBbCtx+k32xydImsHxvFgsOCuFqCTGIxhzRNbgSL1Z2CKhzxwT92kQMhxort9N/fZM2CkRCPPs5wzQSamtzEHA==", + "dev": true, + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.22.tgz", + "integrity": "sha512-vK912As725haT313ANZZZN+0EysEEQXWC/+YE4rQvOQzLuxAQc2tjbzlAFREx3C8+uMuZj/q7E5gyVB7TzpcTA==", + "dev": true, + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.22.tgz", + "integrity": "sha512-/mbJdXTW7MTcsPhtfDsDyPEOju9EOABvCjeUU2OJ7fWpX/Em/H3WYDa86tzLUbcVg++BScQDzqV/7RYw5XNY0g==", + "dev": true, + "optional": true + }, + "esbuild-wasm": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.14.22.tgz", + "integrity": "sha512-FOSAM29GN1fWusw0oLMv6JYhoheDIh5+atC72TkJKfIUMID6yISlicoQSd9gsNSFsNBvABvtE2jR4JB1j4FkFw==", + "dev": true + }, + "esbuild-windows-32": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.22.tgz", + "integrity": "sha512-1vRIkuvPTjeSVK3diVrnMLSbkuE36jxA+8zGLUOrT4bb7E/JZvDRhvtbWXWaveUc/7LbhaNFhHNvfPuSw2QOQg==", + "dev": true, + "optional": true + }, + "esbuild-windows-64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.22.tgz", + "integrity": "sha512-AxjIDcOmx17vr31C5hp20HIwz1MymtMjKqX4qL6whPj0dT9lwxPexmLj6G1CpR3vFhui6m75EnBEe4QL82SYqw==", + "dev": true, + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.14.22", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.22.tgz", + "integrity": "sha512-5wvQ+39tHmRhNpu2Fx04l7QfeK3mQ9tKzDqqGR8n/4WUxsFxnVLfDRBGirIfk4AfWlxk60kqirlODPoT5LqMUg==", + "dev": true, + "optional": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "devOptional": true + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "devOptional": true + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", + "dev": true + }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, + "eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true + }, + "eventsource": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.0.tgz", + "integrity": "sha512-VSJjT5oCNrFvCS6igjzPAt5hBzQ2qPBFIbJ03zLI9SE0mxwZpMw6BfJrbFHm1a141AavMEB8JHmBhWAd66PfCg==", + "requires": { + "original": "^1.0.0" + } + }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "optional": true + }, + "express": { + "version": "4.17.3", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.3.tgz", + "integrity": "sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg==", + "dev": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.19.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.4.2", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.9.7", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.17.2", + "serve-static": "1.14.2", + "setprototypeof": "1.2.0", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", + "dev": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "devOptional": true + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "optional": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "devOptional": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "devOptional": true + }, + "fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fetch-cookie": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/fetch-cookie/-/fetch-cookie-0.11.0.tgz", + "integrity": "sha512-BQm7iZLFhMWFy5CZ/162sAGjBfdNWb7a8LEqqnzsHFhxT/X/SVj/z2t2nu3aJvjlbQkrAlTUApplPRjWyH4mhA==", + "requires": { + "tough-cookie": "^2.3.3 || ^3.0.1 || ^4.0.0" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "fileset": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/fileset/-/fileset-2.0.3.tgz", + "integrity": "sha1-jnVIqW08wjJ+5eZ0FocjozO7oqA=", + "dev": true, + "requires": { + "glob": "^7.0.3", + "minimatch": "^3.0.3" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "requires": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "devOptional": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flatted": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.5.tgz", + "integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==", + "dev": true + }, + "follow-redirects": { + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "optional": true + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "optional": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true + }, + "fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "dev": true + }, + "fs-extra": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "devOptional": true + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "devOptional": true + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "devOptional": true + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0" + } + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "devOptional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + }, + "globby": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", + "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", + "dev": true, + "requires": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" + } + }, + "graceful-fs": { + "version": "4.2.9", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", + "dev": true + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "optional": true + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "optional": true, + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "optional": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "optional": true + } + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "devOptional": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true + } + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "devOptional": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, + "hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "requires": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "dev": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", + "dev": true + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", + "dev": true + }, + "http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + } + }, + "http-parser-js": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.6.tgz", + "integrity": "sha512-vDlkRPDJn93swjcjqMSaGSPABbIarsr1TLAui/gLDXzV5VsJNdXNzMYDyNBLQkjWQCJ1uizu8T2oDMhmGt0PRA==", + "dev": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "http-proxy-middleware": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz", + "integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==", + "dev": true, + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "dev": true, + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "requires": {} + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "ignore-walk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz", + "integrity": "sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==", + "dev": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=", + "optional": true + }, + "immutable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz", + "integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true + }, + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true + }, + "infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "devOptional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "dev": true + }, + "inquirer": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.0.tgz", + "integrity": "sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.2.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "rxjs": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", + "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", + "dev": true, + "requires": { + "tslib": "^2.1.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true + }, + "ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", + "dev": true + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", + "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "devOptional": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "devOptional": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU=", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-cwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", + "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", + "optional": true + }, + "is-path-in-cwd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", + "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", + "optional": true, + "requires": { + "is-path-inside": "^1.0.0" + } + }, + "is-path-inside": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", + "optional": true, + "requires": { + "path-is-inside": "^1.0.1" + } + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "optional": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "devOptional": true + }, + "isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "optional": true + }, + "istanbul-api": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/istanbul-api/-/istanbul-api-2.1.7.tgz", + "integrity": "sha512-LYTOa2UrYFyJ/aSczZi/6lBykVMjCCvUmT64gOe+jPZFy4w6FYfPGqFT2IiQ2BxVHHDOvCD7qrIXb0EOh4uGWw==", + "dev": true, + "requires": { + "async": "^2.6.2", + "compare-versions": "^3.4.0", + "fileset": "^2.0.3", + "istanbul-lib-coverage": "^2.0.5", + "istanbul-lib-hook": "^2.0.7", + "istanbul-lib-instrument": "^3.3.0", + "istanbul-lib-report": "^2.0.8", + "istanbul-lib-source-maps": "^3.0.6", + "istanbul-reports": "^2.2.5", + "js-yaml": "^3.13.1", + "make-dir": "^2.1.0", + "minimatch": "^3.0.4", + "once": "^1.4.0" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", + "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", + "dev": true, + "requires": { + "@babel/generator": "^7.4.0", + "@babel/parser": "^7.4.3", + "@babel/template": "^7.4.0", + "@babel/traverse": "^7.4.3", + "@babel/types": "^7.4.0", + "istanbul-lib-coverage": "^2.0.5", + "semver": "^6.0.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-hook": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", + "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", + "dev": true, + "requires": { + "append-transform": "^1.0.0" + } + }, + "istanbul-lib-instrument": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz", + "integrity": "sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", + "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "supports-color": "^6.1.0" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", + "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", + "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^2.0.5", + "make-dir": "^2.1.0", + "rimraf": "^2.6.3", + "source-map": "^0.6.1" + }, + "dependencies": { + "istanbul-lib-coverage": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", + "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", + "dev": true + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.7.tgz", + "integrity": "sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0" + } + }, + "jasmine": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-2.8.0.tgz", + "integrity": "sha1-awicChFXax8W3xG4AUbZHU6Lij4=", + "optional": true, + "requires": { + "exit": "^0.1.2", + "glob": "^7.0.6", + "jasmine-core": "~2.8.0" + }, + "dependencies": { + "jasmine-core": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.8.0.tgz", + "integrity": "sha1-vMl5rh+f0FcB5F5S5l06XWPxok4=", + "optional": true + } + } + }, + "jasmine-core": { + "version": "3.99.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.99.1.tgz", + "integrity": "sha512-Hu1dmuoGcZ7AfyynN3LsfruwMbxMALMka+YtZeGoLuDEySVmVAPaonkNoBRIw/ectu8b9tVQCJNgp4a4knp+tg==", + "dev": true + }, + "jasmine-spec-reporter": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jasmine-spec-reporter/-/jasmine-spec-reporter-4.2.1.tgz", + "integrity": "sha512-FZBoZu7VE5nR7Nilzy+Np8KuVIOxF4oXDPDknehCYBDE080EnlPu0afdZNmpGDBRCUBv3mj5qgqCRmk6W/K8vg==", + "dev": true, + "requires": { + "colors": "1.1.2" + } + }, + "jasminewd2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/jasminewd2/-/jasminewd2-2.2.0.tgz", + "integrity": "sha1-43zwsX8ZnM4jvqcbIDk5Uka07E4=", + "optional": true + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jquery": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", + "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "devOptional": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "devOptional": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "optional": true + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "optional": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "optional": true + }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, + "jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", + "dev": true + }, + "jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + } + }, + "jszip": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", + "integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==", + "optional": true, + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "set-immediate-shim": "~1.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "jwt-decode": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-3.1.2.tgz", + "integrity": "sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==" + }, + "karma": { + "version": "6.3.17", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.17.tgz", + "integrity": "sha512-2TfjHwrRExC8yHoWlPBULyaLwAFmXmxQrcuFImt/JsAsSZu1uOWTZ1ZsWjqQtWpHLiatJOHL5jFjXSJIgCd01g==", + "dev": true, + "requires": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.2.0", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "requires": { + "rimraf": "^3.0.0" + } + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } + } + }, + "karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "requires": { + "which": "^1.2.1" + } + }, + "karma-coverage-istanbul-reporter": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/karma-coverage-istanbul-reporter/-/karma-coverage-istanbul-reporter-2.1.1.tgz", + "integrity": "sha512-CH8lTi8+kKXGvrhy94+EkEMldLCiUA0xMOiL31vvli9qK0T+qcXJAwWBRVJWnVWxYkTmyWar8lPz63dxX6/z1A==", + "dev": true, + "requires": { + "istanbul-api": "^2.1.6", + "minimatch": "^3.0.4" + } + }, + "karma-jasmine": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", + "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==", + "dev": true, + "requires": { + "jasmine-core": "^3.3" + } + }, + "karma-jasmine-html-reporter": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-1.7.0.tgz", + "integrity": "sha512-pzum1TL7j90DTE86eFt48/s12hqwQuiD+e5aXx2Dc9wDEn2LfGq6RoAxEZZjFiN0RDSCOnosEKRZWxbQ+iMpQQ==", + "dev": true, + "requires": {} + }, + "karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "requires": { + "source-map-support": "^0.5.5" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "klona": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz", + "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==", + "dev": true + }, + "less": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.2.tgz", + "integrity": "sha512-EoQp/Et7OSOVu0aJknJOtlXZsnr8XE8KwuzTHOLeVSEx8pVWUICc8Q0VYRHgzyjX78nMEyC/oztWFbgyhtNfDA==", + "dev": true, + "requires": { + "copy-anything": "^2.0.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^2.5.2", + "parse-node-version": "^1.0.1", + "source-map": "~0.6.0", + "tslib": "^2.3.0" + }, + "dependencies": { + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "optional": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true + } + } + }, + "less-loader": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-10.2.0.tgz", + "integrity": "sha512-AV5KHWvCezW27GT90WATaDnfXBv99llDbtaj4bshq6DvAihMdNjaPDcUMa6EXKLRF+P2opFenJp89BXg91XLYg==", + "dev": true, + "requires": { + "klona": "^2.0.4" + } + }, + "license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "requires": { + "webpack-sources": "^3.0.0" + } + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "optional": true, + "requires": { + "immediate": "~3.0.5" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "loader-runner": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", + "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", + "dev": true + }, + "loader-utils": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz", + "integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "devOptional": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "log4js": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.4.4.tgz", + "integrity": "sha512-ncaWPsuw9Vl1CKA406hVnJLGQKy1OHx6buk8J4rE2lVW+NW5Y82G5/DIloO7NkqLOUtNPEANaWC1kZYVjXssPw==", + "dev": true, + "requires": { + "date-format": "^4.0.6", + "debug": "^4.3.4", + "flatted": "^3.2.5", + "rfdc": "^1.3.0", + "streamroller": "^3.0.6" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "magic-string": { + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz", + "integrity": "sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA==", + "dev": true, + "requires": { + "sourcemap-codec": "^1.4.4" + } + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "optional": true + }, + "make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, + "requires": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", + "dev": true + }, + "memfs": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz", + "integrity": "sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw==", + "dev": true, + "requires": { + "fs-monkey": "1.0.3" + } + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", + "dev": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "devOptional": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "devOptional": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.3.tgz", + "integrity": "sha512-YseMB8cs8U/KCaAGQoqYmfUuhhGW0a9p9XvWXrxVOkE3/IiISTLw4ALNt7JR5B2eYauFM+PQGSbXMDmVbR7Tfw==", + "dev": true, + "requires": { + "schema-utils": "^4.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "devOptional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "devOptional": true + }, + "minipass": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz", + "integrity": "sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, + "requires": { + "encoding": "^0.1.12", + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "requires": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "devOptional": true + }, + "multicast-dns": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", + "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "dev": true, + "requires": { + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "nanoid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.2.tgz", + "integrity": "sha512-CuHBogktKwpm5g2sRgv83jEy2ijFzBwMoYA60orPDR7ynsLijJDqgsi4RDGj3OJpy3Ieb+LYwiRmIOGyytgITA==", + "dev": true + }, + "needle": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", + "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", + "dev": true, + "optional": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "ngx-toastr": { + "version": "14.2.2", + "resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-14.2.2.tgz", + "integrity": "sha512-/Ajr9E0llr51Zij8WgnxQpe7a5JK+k1n07/uWJcQ112OBH0GCktHi8M8QfGvw5Ih67hG8iowrT+aHXHS49gZcQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "optional": true, + "requires": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "node-forge": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", + "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", + "dev": true + }, + "node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "dependencies": { + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", + "dev": true, + "optional": true + }, + "node-releases": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", + "integrity": "sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==", + "dev": true + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, + "npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-install-checks": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz", + "integrity": "sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w==", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "npm-package-arg": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz", + "integrity": "sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q==", + "dev": true, + "requires": { + "hosted-git-info": "^4.0.1", + "semver": "^7.3.4", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-packlist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz", + "integrity": "sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==", + "dev": true, + "requires": { + "glob": "^7.1.6", + "ignore-walk": "^4.0.1", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz", + "integrity": "sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA==", + "dev": true, + "requires": { + "npm-install-checks": "^4.0.0", + "npm-normalize-package-bin": "^1.0.1", + "npm-package-arg": "^8.1.2", + "semver": "^7.3.4" + } + }, + "npm-registry-fetch": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-12.0.2.tgz", + "integrity": "sha512-Df5QT3RaJnXYuOwtXBXS9BWs+tHH2olvkCLh6jcR/b/u3DvPMlp3J0TvvYwplPKxHMOwfg287PYih9QqaVFoKA==", + "dev": true, + "requires": { + "make-fetch-happen": "^10.0.1", + "minipass": "^3.1.6", + "minipass-fetch": "^1.4.1", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^8.1.5" + }, + "dependencies": { + "@npmcli/fs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-2.1.0.tgz", + "integrity": "sha512-DmfBvNXGaetMxj9LTp8NAN9vEidXURrf5ZTslQzEAi/6GbW+4yjaLFQc6Tue5cpZ9Frlk4OBo/Snf1Bh/S7qTQ==", + "dev": true, + "requires": { + "@gar/promisify": "^1.1.3", + "semver": "^7.3.5" + } + }, + "@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true + }, + "cacache": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.0.3.tgz", + "integrity": "sha512-eC7wYodNCVb97kuHGk5P+xZsvUJHkhSEOyNwkenqQPAsOtrTjvWOE5vSPNBpz9d8X3acIf6w2Ub5s4rvOCTs4g==", + "dev": true, + "requires": { + "@npmcli/fs": "^2.1.0", + "@npmcli/move-file": "^1.1.2", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "glob": "^7.2.0", + "infer-owner": "^1.0.4", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "mkdirp": "^1.0.4", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.11", + "unique-filename": "^1.1.1" + } + }, + "http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "requires": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + } + }, + "lru-cache": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.7.1.tgz", + "integrity": "sha512-cRffBiTW8s73eH4aTXqBcTLU0xQnwGV3/imttRHGWCrbergmnK4D6JXQd8qin5z43HnDwRI+o7mVW0LEB+tpAw==", + "dev": true + }, + "make-fetch-happen": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.1.1.tgz", + "integrity": "sha512-3/mCljDQNjmrP7kl0vhS5WVlV+TvSKoZaFhdiYV7MOijEnrhrjaVnqbp/EY/7S+fhUB2KpH7j8c1iRsIOs+kjw==", + "dev": true, + "requires": { + "agentkeepalive": "^4.2.1", + "cacache": "^16.0.2", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^3.1.6", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^2.0.3", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.1.1", + "ssri": "^8.0.1" + }, + "dependencies": { + "minipass-fetch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-2.1.0.tgz", + "integrity": "sha512-H9U4UVBGXEyyWJnqYDCLp1PwD8XIkJ4akNHp1aGVI+2Ym7wQMlxDKi4IB4JbmyU+pl9pEs/cVrK6cOuvmbK4Sg==", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^3.1.6", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + } + } + } + } + } + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "npmlog": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.1.tgz", + "integrity": "sha512-BTHDvY6nrRHuRfyjt1MAufLxYdVXZfd099H4+i1f0lPywNQyI4foeNXJRObB/uy+TYqUW0vAD9gbdSOXPst7Eg==", + "dev": true, + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.0", + "set-blocking": "^2.0.0" + } + }, + "nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "devOptional": true + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "oidc-client": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.11.5.tgz", + "integrity": "sha512-LcKrKC8Av0m/KD/4EFmo9Sg8fSQ+WFJWBrmtWd+tZkNn3WT/sQG3REmPANE9tzzhbjW6VkTNy4xhAXCfPApAOg==", + "requires": { + "acorn": "^7.4.1", + "base64-js": "^1.5.1", + "core-js": "^3.8.3", + "crypto-js": "^4.0.0", + "serialize-javascript": "^4.0.0" + }, + "dependencies": { + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "requires": { + "randombytes": "^2.1.0" + } + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "devOptional": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.0.tgz", + "integrity": "sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "requires": { + "url-parse": "^1.4.3" + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "devOptional": true + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "devOptional": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "devOptional": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } + }, + "p-retry": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz", + "integrity": "sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA==", + "dev": true, + "requires": { + "@types/retry": "^0.12.0", + "retry": "^0.13.1" + }, + "dependencies": { + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true + } + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "devOptional": true + }, + "pacote": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-12.0.3.tgz", + "integrity": "sha512-CdYEl03JDrRO3x18uHjBYA9TyoW8gy+ThVcypcDkxPtKlw76e4ejhYB6i9lJ+/cebbjpqPW/CijjqxwDTts8Ow==", + "dev": true, + "requires": { + "@npmcli/git": "^2.1.0", + "@npmcli/installed-package-contents": "^1.0.6", + "@npmcli/promise-spawn": "^1.2.0", + "@npmcli/run-script": "^2.0.0", + "cacache": "^15.0.5", + "chownr": "^2.0.0", + "fs-minipass": "^2.1.0", + "infer-owner": "^1.0.4", + "minipass": "^3.1.3", + "mkdirp": "^1.0.3", + "npm-package-arg": "^8.0.1", + "npm-packlist": "^3.0.0", + "npm-pick-manifest": "^6.0.0", + "npm-registry-fetch": "^12.0.0", + "promise-retry": "^2.0.1", + "read-package-json-fast": "^2.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.1.0" + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "devOptional": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true + }, + "parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true + }, + "parse5-html-rewriting-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-6.0.1.tgz", + "integrity": "sha512-vwLQzynJVEfUlURxgnf51yAJDQTtVpNyGD8tKi2Za7m+akukNHxCcUQMAa/mUGLhCeicFdpy7Tlvj8ZNKadprg==", + "dev": true, + "requires": { + "parse5": "^6.0.1", + "parse5-sax-parser": "^6.0.1" + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + } + }, + "parse5-sax-parser": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-6.0.1.tgz", + "integrity": "sha512-kXX+5S81lgESA0LsDuGjAlBybImAChYRMT+/uKCEXFBFOeEhS52qUCydGhU3qLRD8D9DVjaUo821WK7DM4iCeg==", + "dev": true, + "requires": { + "parse5": "^6.0.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "devOptional": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "devOptional": true + }, + "path-is-inside": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", + "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", + "optional": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "devOptional": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", + "optional": true + }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "devOptional": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "optional": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "optional": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "requires": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0", + "nice-napi": "^1.0.2" + } + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "popper.js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", + "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", + "peer": true + }, + "portfinder": { + "version": "1.0.28", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", + "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "dev": true, + "requires": { + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + } + } + }, + "postcss": { + "version": "8.4.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz", + "integrity": "sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==", + "dev": true, + "requires": { + "nanoid": "^3.1.30", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.1" + } + }, + "postcss-attribute-case-insensitive": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz", + "integrity": "sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.2" + } + }, + "postcss-color-functional-notation": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.2.tgz", + "integrity": "sha512-DXVtwUhIk4f49KK5EGuEdgx4Gnyj6+t2jBSEmxvpIK9QI40tWrpS2Pua8Q7iIZWBrki2QOaeUdEaLPPa91K0RQ==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-hex-alpha": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.3.tgz", + "integrity": "sha512-fESawWJCrBV035DcbKRPAVmy21LpoyiXdPTuHUfWJ14ZRjY7Y7PA6P4g8z6LQGYhU1WAxkTxjIjurXzoe68Glw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-color-rebeccapurple": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz", + "integrity": "sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-media": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz", + "integrity": "sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g==", + "dev": true, + "requires": {} + }, + "postcss-custom-properties": { + "version": "12.1.5", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz", + "integrity": "sha512-FHbbB/hRo/7cxLGkc2NS7cDRIDN1oFqQnUKBiyh4b/gwk8DD8udvmRDpUhEK836kB8ggUCieHVOvZDnF9XhI3g==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-custom-selectors": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz", + "integrity": "sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-dir-pseudo-class": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.4.tgz", + "integrity": "sha512-I8epwGy5ftdzNWEYok9VjW9whC4xnelAtbajGv4adql4FIF09rnrxnA9Y8xSHN47y7gqFIv10C5+ImsLeJpKBw==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-double-position-gradients": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.1.1.tgz", + "integrity": "sha512-jM+CGkTs4FcG53sMPjrrGE0rIvLDdCrqMzgDC5fLI7JHDO7o6QG8C5TQBtExb13hdBdoH9C2QVbG4jo2y9lErQ==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-env-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.6.tgz", + "integrity": "sha512-kpA6FsLra+NqcFnL81TnsU+Z7orGtDTxcOhl6pwXeEq1yFPpRMkCDpHhrz8CFQDr/Wfm0jLiNQ1OsGGPjlqPwA==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-focus-visible": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.4.tgz", + "integrity": "sha512-QcKuUU/dgNsstIK6HELFRT5Y3lbrMLEOwG+A4s5cA+fx3A3y/JTq3X9LaOj3OC3ALH0XqyrgQIgey/MIZ8Wczw==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-focus-within": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.4.tgz", + "integrity": "sha512-vvjDN++C0mu8jz4af5d52CB184ogg/sSxAFS+oUJQq2SuCe7T5U2iIsVJtsCp2d6R4j0jr5+q3rPkBVZkXD9fQ==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "requires": {} + }, + "postcss-gap-properties": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.3.tgz", + "integrity": "sha512-rPPZRLPmEKgLk/KlXMqRaNkYTUpE7YC+bOIQFN5xcu1Vp11Y4faIXv6/Jpft6FMnl6YRxZqDZG0qQOW80stzxQ==", + "dev": true, + "requires": {} + }, + "postcss-image-set-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.6.tgz", + "integrity": "sha512-KfdC6vg53GC+vPd2+HYzsZ6obmPqOk6HY09kttU19+Gj1nC3S3XBVEXDHxkhxTohgZqzbUb94bKXvKDnYWBm/A==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-import": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.0.2.tgz", + "integrity": "sha512-BJ2pVK4KhUyMcqjuKs9RijV5tatNzNa73e/32aBVE/ejYPe37iH+6vAu9WvqUkB5OAYgLHzbSvzHnorybJCm9g==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + } + }, + "postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "dev": true, + "requires": {} + }, + "postcss-lab-function": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.1.2.tgz", + "integrity": "sha512-isudf5ldhg4fk16M8viAwAbg6Gv14lVO35N3Z/49NhbwPQ2xbiEoHgrRgpgQojosF4vF7jY653ktB6dDrUOR8Q==", + "dev": true, + "requires": { + "@csstools/postcss-progressive-custom-properties": "^1.1.0", + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dev": true, + "requires": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + } + }, + "postcss-logical": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.4.tgz", + "integrity": "sha512-RHXxplCeLh9VjinvMrZONq7im4wjWGlRJAqmAVLXyZaXwfDWP73/oq4NdIp+OZwhQUMj0zjqDfM5Fj7qby+B4g==", + "dev": true, + "requires": {} + }, + "postcss-media-minmax": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz", + "integrity": "sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ==", + "dev": true, + "requires": {} + }, + "postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.4" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-nesting": { + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.1.3.tgz", + "integrity": "sha512-wUC+/YCik4wH3StsbC5fBG1s2Z3ZV74vjGqBFYtmYKlVxoio5TYGM06AiaKkQPPlkXWn72HKfS7Cw5PYxnoXSw==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-overflow-shorthand": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.3.tgz", + "integrity": "sha512-CxZwoWup9KXzQeeIxtgOciQ00tDtnylYIlJBBODqkgS/PU2jISuWOL/mYLHmZb9ZhZiCaNKsCRiLp22dZUtNsg==", + "dev": true, + "requires": {} + }, + "postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "requires": {} + }, + "postcss-place": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.4.tgz", + "integrity": "sha512-MrgKeiiu5OC/TETQO45kV3npRjOFxEHthsqGtkh3I1rPbZSbXGD/lZVi9j13cYh+NA8PIAPyk6sGjT9QbRyvSg==", + "dev": true, + "requires": { + "postcss-value-parser": "^4.2.0" + } + }, + "postcss-preset-env": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.2.3.tgz", + "integrity": "sha512-Ok0DhLfwrcNGrBn8sNdy1uZqWRk/9FId0GiQ39W4ILop5GHtjJs8bu1MY9isPwHInpVEPWjb4CEcEaSbBLpfwA==", + "dev": true, + "requires": { + "autoprefixer": "^10.4.2", + "browserslist": "^4.19.1", + "caniuse-lite": "^1.0.30001299", + "css-blank-pseudo": "^3.0.2", + "css-has-pseudo": "^3.0.3", + "css-prefers-color-scheme": "^6.0.2", + "cssdb": "^5.0.0", + "postcss-attribute-case-insensitive": "^5.0.0", + "postcss-color-functional-notation": "^4.2.1", + "postcss-color-hex-alpha": "^8.0.2", + "postcss-color-rebeccapurple": "^7.0.2", + "postcss-custom-media": "^8.0.0", + "postcss-custom-properties": "^12.1.2", + "postcss-custom-selectors": "^6.0.0", + "postcss-dir-pseudo-class": "^6.0.3", + "postcss-double-position-gradients": "^3.0.4", + "postcss-env-function": "^4.0.4", + "postcss-focus-visible": "^6.0.3", + "postcss-focus-within": "^5.0.3", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^3.0.2", + "postcss-image-set-function": "^4.0.4", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^4.0.3", + "postcss-logical": "^5.0.3", + "postcss-media-minmax": "^5.0.0", + "postcss-nesting": "^10.1.2", + "postcss-overflow-shorthand": "^3.0.2", + "postcss-page-break": "^3.0.4", + "postcss-place": "^7.0.3", + "postcss-pseudo-class-any-link": "^7.0.2", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^5.0.0" + } + }, + "postcss-pseudo-class-any-link": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.1.1.tgz", + "integrity": "sha512-JRoLFvPEX/1YTPxRxp1JO4WxBVXJYrSY7NHeak5LImwJ+VobFMwYDQHvfTXEpcn+7fYIeGkC29zYFhFWIZD8fg==", + "dev": true, + "requires": { + "postcss-selector-parser": "^6.0.9" + } + }, + "postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "requires": {} + }, + "postcss-selector-not": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz", + "integrity": "sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "postcss-selector-parser": { + "version": "6.0.9", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz", + "integrity": "sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ==", + "dev": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "devOptional": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", + "dev": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "protractor": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/protractor/-/protractor-7.0.0.tgz", + "integrity": "sha512-UqkFjivi4GcvUQYzqGYNe0mLzfn5jiLmO8w9nMhQoJRLhy2grJonpga2IWhI6yJO30LibWXJJtA4MOIZD2GgZw==", + "optional": true, + "requires": { + "@types/q": "^0.0.32", + "@types/selenium-webdriver": "^3.0.0", + "blocking-proxy": "^1.0.0", + "browserstack": "^1.5.1", + "chalk": "^1.1.3", + "glob": "^7.0.3", + "jasmine": "2.8.0", + "jasminewd2": "^2.1.0", + "q": "1.4.1", + "saucelabs": "^1.5.0", + "selenium-webdriver": "3.6.0", + "source-map-support": "~0.4.0", + "webdriver-js-extender": "2.1.0", + "webdriver-manager": "^12.1.7", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "optional": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "optional": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "optional": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "optional": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "optional": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "optional": true + }, + "source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "optional": true, + "requires": { + "source-map": "^0.5.6" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "optional": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "optional": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "optional": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + } + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "optional": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "optional": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "optional": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true + } + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true, + "optional": true + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "q": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", + "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", + "optional": true + }, + "qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true + }, + "qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "dev": true + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true + }, + "raw-body": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz", + "integrity": "sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g==", + "dev": true, + "requires": { + "bytes": "3.1.2", + "http-errors": "1.8.1", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha1-5mTvMRYRZsl1HNvo28+GtftY93Q=", + "dev": true, + "requires": { + "pify": "^2.3.0" + } + }, + "read-package-json-fast": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz", + "integrity": "sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ==", + "dev": true, + "requires": { + "json-parse-even-better-errors": "^2.3.0", + "npm-normalize-package-bin": "^1.0.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==" + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "regenerate-unicode-properties": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.0.1.tgz", + "integrity": "sha512-vn5DU6yg6h8hP/2OkQo3K7uVILvY4iu0oI4t3HFa81UPkhGJwkRwM10JEc3upjdhHjs/k8GJY1sRBhk5sr69Bw==", + "dev": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==", + "dev": true + }, + "regenerator-transform": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", + "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "dev": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "regexp.prototype.flags": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz", + "integrity": "sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "regexpu-core": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.0.1.tgz", + "integrity": "sha512-CriEZlrKK9VJw/xQGJpQM5rY88BtuL8DM+AEwvcThHilbxiTAy8vq4iJnd2tqq8wLmjbGZzP7ZcKFjbGkmEFrw==", + "dev": true, + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.0.1", + "regjsgen": "^0.6.0", + "regjsparser": "^0.8.2", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.0.0" + } + }, + "regjsgen": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.6.0.tgz", + "integrity": "sha512-ozE883Uigtqj3bx7OhL1KNbCzGyW2NQZPl6Hs09WTvCuZD5sTI4JY58bkbQWa/Y9hxIsvJ3M8Nbf7j54IqeZbA==", + "dev": true + }, + "regjsparser": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.8.4.tgz", + "integrity": "sha512-J3LABycON/VNEu3abOviqGHuB/LOtOQj8SKmfP9anY5GfAVw/SPjwzSjxGjbZXIxbGfqTHtJw58C2Li/WkStmA==", + "dev": true, + "requires": { + "jsesc": "~0.5.0" + }, + "dependencies": { + "jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", + "dev": true + } + } + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "optional": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "optional": true + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "optional": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "optional": true + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "devOptional": true + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "optional": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, + "resolve": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", + "integrity": "sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw==", + "devOptional": true, + "requires": { + "is-core-module": "^2.8.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "requires": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" + } + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "devOptional": true + }, + "sass": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.0.tgz", + "integrity": "sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==", + "dev": true, + "requires": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "sass-loader": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.4.0.tgz", + "integrity": "sha512-7xN+8khDIzym1oL9XyS6zP6Ges+Bo2B2xbPrjdMHEYyV3AQYhd/wXeru++3ODHF0zMjYmVadblSKrPrjEkL8mg==", + "dev": true, + "requires": { + "klona": "^2.0.4", + "neo-async": "^2.6.2" + } + }, + "saucelabs": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/saucelabs/-/saucelabs-1.5.0.tgz", + "integrity": "sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ==", + "optional": true, + "requires": { + "https-proxy-agent": "^2.2.1" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "optional": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "optional": true, + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + } + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "devOptional": true + }, + "schema-utils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", + "dev": true + }, + "selenium-webdriver": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz", + "integrity": "sha512-WH7Aldse+2P5bbFBO4Gle/nuQOdVwpHMTL6raL3uuBj/vPG07k6uzt3aiahu352ONBr5xXh0hDlM3LhtXPOC4Q==", + "optional": true, + "requires": { + "jszip": "^3.1.3", + "rimraf": "^2.5.4", + "tmp": "0.0.30", + "xml2js": "^0.4.17" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "tmp": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.30.tgz", + "integrity": "sha1-ckGdSovn1s51FI/YsyTlk6cRwu0=", + "optional": true, + "requires": { + "os-tmpdir": "~1.0.1" + } + } + } + }, + "selfsigned": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.1.tgz", + "integrity": "sha512-LmME957M1zOsUhG+67rAjKfiWFox3SBxE/yymatMZsAx+oMrJ0YQ8AToOnyCm7xbeg2ep37IHLxdu0o2MavQOQ==", + "dev": true, + "requires": { + "node-forge": "^1" + } + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "semver-dsl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/semver-dsl/-/semver-dsl-1.0.1.tgz", + "integrity": "sha1-02eN5VVeimH2Ke7QJTZq5fJzQKA=", + "dev": true, + "requires": { + "semver": "^5.3.0" + }, + "dependencies": { + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, + "send": { + "version": "0.17.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz", + "integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==", + "dev": true, + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "1.8.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + } + } + }, + "serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", + "dev": true, + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + } + } + }, + "serve-static": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz", + "integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==", + "dev": true, + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.2" + } + }, + "service": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/service/-/service-0.1.4.tgz", + "integrity": "sha1-0Kuf+8K51Yda+LAd7DYzl4RSW0Q=", + "requires": { + "daemon": ">=0.3.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "devOptional": true + }, + "set-immediate-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", + "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", + "optional": true + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "socket.io": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + } + }, + "socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", + "dev": true + }, + "socket.io-parser": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", + "dev": true, + "requires": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + } + }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "socks": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", + "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "dev": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz", + "integrity": "sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew==", + "dev": true, + "requires": { + "agent-base": "^6.0.2", + "debug": "^4.3.1", + "socks": "^2.6.1" + } + }, + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + }, + "source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true + }, + "source-map-loader": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz", + "integrity": "sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA==", + "dev": true, + "requires": { + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "dev": true, + "requires": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "devOptional": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true + } + } + }, + "sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "sprintf-js": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", + "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==", + "dev": true + }, + "sshpk": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", + "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", + "optional": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, + "requires": { + "minipass": "^3.1.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", + "dev": true + }, + "streamroller": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.0.6.tgz", + "integrity": "sha512-Qz32plKq/MZywYyhEatxyYc8vs994Gz0Hu2MSYXXLD233UyPeIeRBZARIIGwFer4Mdb8r3Y2UqKkgyDghM6QCg==", + "dev": true, + "requires": { + "date-format": "^4.0.6", + "debug": "^4.3.4", + "fs-extra": "^10.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + } + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "devOptional": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "devOptional": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, + "stylus": { + "version": "0.56.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.56.0.tgz", + "integrity": "sha512-Ev3fOb4bUElwWu4F9P9WjnnaSpc8XB9OFHSFZSKMFL1CE1oM+oFXWEgAqPmmZIyhBihuqIQlFsVTypiiS9RxeA==", + "dev": true, + "requires": { + "css": "^3.0.0", + "debug": "^4.3.2", + "glob": "^7.1.6", + "safer-buffer": "^2.1.2", + "sax": "~1.2.4", + "source-map": "^0.7.3" + } + }, + "stylus-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-6.2.0.tgz", + "integrity": "sha512-5dsDc7qVQGRoc6pvCL20eYgRUxepZ9FpeK28XhdXaIPP6kXr6nI1zAAKFQgP5OBkOfKaURp4WUpJzspg1f01Gg==", + "dev": true, + "requires": { + "fast-glob": "^3.2.7", + "klona": "^2.0.4", + "normalize-path": "^3.0.0" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "devOptional": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "devOptional": true + }, + "svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "requires": { + "svg.js": "^2.0.1" + } + }, + "svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha1-iqmUawqOJ4V6XEChDrpAkeVpHxI=", + "requires": { + "svg.js": ">=2.3.x" + } + }, + "svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha1-kQCOFROJ3ZIwd5/L5uLJo2LRwgM=", + "requires": { + "svg.js": "^2.2.5" + } + }, + "svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "requires": { + "svg.js": "^2.4.0" + } + }, + "svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "requires": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "dependencies": { + "svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "requires": { + "svg.js": "^2.2.5" + } + } + } + }, + "svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "requires": { + "svg.js": "^2.6.5" + } + }, + "symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true + }, + "tar": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", + "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, + "terser": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.11.0.tgz", + "integrity": "sha512-uCA9DLanzzWSsN1UirKwylhhRz3aKPInlfmpGfw8VN6jHsAtu8HJtIpeeHHK23rxnE/cDc+yvmq5wqkIC6Kn0A==", + "dev": true, + "requires": { + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.20" + }, + "dependencies": { + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + } + } + }, + "terser-webpack-plugin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", + "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "dev": true, + "requires": { + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0", + "source-map": "^0.6.1", + "terser": "^5.7.2" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toastr": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/toastr/-/toastr-2.1.4.tgz", + "integrity": "sha1-i0O+ZPudDEFIcURvLbjoyk6V8YE=", + "requires": { + "jquery": ">=1.12.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true + }, + "tough-cookie": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" + }, + "dependencies": { + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true + }, + "ts-node": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.4.1.tgz", + "integrity": "sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw==", + "optional": true, + "requires": { + "arg": "^4.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "source-map-support": "^0.5.6", + "yn": "^3.0.0" + } + }, + "tslib": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + }, + "tslint": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", + "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", + "devOptional": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.8.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "devOptional": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "devOptional": true + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "devOptional": true + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "devOptional": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "devOptional": true + } + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "optional": true + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "typescript": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz", + "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==", + "devOptional": true + }, + "ua-parser-js": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.31.tgz", + "integrity": "sha512-qLK/Xe9E2uzmYI3qLeOmI0tEOt+TBBQyUIAh4aAgU05FVYzeZrKUdkAZfBNVGRaHVgV0TDkdEngJSw/SyQchkQ==", + "dev": true + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz", + "integrity": "sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw==", + "dev": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz", + "integrity": "sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ==", + "dev": true + }, + "unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "devOptional": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "devOptional": true + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "dev": true, + "requires": { + "builtins": "^1.0.3" + } + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "optional": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", + "dev": true + }, + "watchpack": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz", + "integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "webdriver-js-extender": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/webdriver-js-extender/-/webdriver-js-extender-2.1.0.tgz", + "integrity": "sha512-lcUKrjbBfCK6MNsh7xaY2UAUmZwe+/ib03AjVOpFobX4O7+83BUveSrLfU0Qsyb1DaKJdQRbuU+kM9aZ6QUhiQ==", + "optional": true, + "requires": { + "@types/selenium-webdriver": "^3.0.0", + "selenium-webdriver": "^3.0.1" + } + }, + "webdriver-manager": { + "version": "12.1.8", + "resolved": "https://registry.npmjs.org/webdriver-manager/-/webdriver-manager-12.1.8.tgz", + "integrity": "sha512-qJR36SXG2VwKugPcdwhaqcLQOD7r8P2Xiv9sfNbfZrKBnX243iAkOueX1yAmeNgIKhJ3YAT/F2gq6IiEZzahsg==", + "optional": true, + "requires": { + "adm-zip": "^0.4.9", + "chalk": "^1.1.1", + "del": "^2.2.0", + "glob": "^7.0.3", + "ini": "^1.3.4", + "minimist": "^1.2.0", + "q": "^1.4.1", + "request": "^2.87.0", + "rimraf": "^2.5.2", + "semver": "^5.3.0", + "xml2js": "^0.4.17" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "optional": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "optional": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "optional": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "optional": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "optional": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "optional": true + } + } + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "webpack": { + "version": "5.70.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.70.0.tgz", + "integrity": "sha512-ZMWWy8CeuTTjCxbeaQI21xSswseF2oNOwc70QSKNePvmxE7XW36i7vpBMYZFAUHPwQiEbNGCEYIOOlyRbdGmxw==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^0.0.51", + "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/wasm-edit": "1.11.1", + "@webassemblyjs/wasm-parser": "1.11.1", + "acorn": "^8.4.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.9.2", + "es-module-lexer": "^0.9.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.1.3", + "watchpack": "^2.3.1", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "acorn": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", + "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "dev": true + }, + "acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "requires": {} + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "schema-utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", + "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-dev-middleware": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.0.tgz", + "integrity": "sha512-MouJz+rXAm9B1OTOYaJnn6rtD/lWZPy2ufQCH3BPs8Rloh/Du6Jze4p7AeLYHkVi0giJnYLaSGDC7S+GM9arhg==", + "dev": true, + "requires": { + "colorette": "^2.0.10", + "memfs": "^3.2.2", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "dependencies": { + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + } + } + }, + "webpack-dev-server": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz", + "integrity": "sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q==", + "dev": true, + "requires": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/serve-index": "^1.9.1", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.2.2", + "ansi-html-community": "^0.0.8", + "bonjour": "^3.5.0", + "chokidar": "^3.5.2", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "default-gateway": "^6.0.3", + "del": "^6.0.0", + "express": "^4.17.1", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.0", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "portfinder": "^1.0.28", + "schema-utils": "^4.0.0", + "selfsigned": "^2.0.0", + "serve-index": "^1.9.1", + "sockjs": "^0.3.21", + "spdy": "^4.0.2", + "strip-ansi": "^7.0.0", + "webpack-dev-middleware": "^5.3.0", + "ws": "^8.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "requires": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "schema-utils": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz", + "integrity": "sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.8.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "strip-ansi": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz", + "integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "dev": true, + "requires": {} + } + } + }, + "webpack-merge": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true + }, + "webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "requires": { + "typed-assert": "^1.0.8" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "optional": true + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "devOptional": true + }, + "ws": { + "version": "7.5.7", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", + "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "requires": {} + }, + "xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==" + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "optional": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "optional": true + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true + }, + "yargs": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", + "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.1.tgz", + "integrity": "sha512-9BK1jFpLzJROCI5TzwZL/TU4gqjK5xiHV/RfWLOahrjAko/e4DJkRDZQXfvqAsiZzzYhgAzbgz6lg48jcm4GLg==", + "dev": true + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "optional": true + }, + "zone.js": { + "version": "0.11.5", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.11.5.tgz", + "integrity": "sha512-D1/7VxEuQ7xk6z/kAROe4SUbd9CzxY4zOwVGnGHerd/SgLIVU5f4esDzQUsOCeArn933BZfWMKydH7l7dPEp0g==", + "requires": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/package.json b/src/Server/Coderr.Server.WebSite/ClientApp/package.json new file mode 100644 index 00000000..1648fb43 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/package.json @@ -0,0 +1,62 @@ +{ + "name": "coderr-frontend", + "version": "3.0.0-rc01", + "scripts": { + "ng": "ng", + "start": "ngserve", + "build": "ng build", + "build:ssr": "ng run coderr-frontend:server:dev", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/animations": "^13.3.0", + "@angular/common": "^13.3.0", + "@angular/core": "13.3.0", + "@angular/forms": "^13.3.0", + "@angular/platform-browser": "^13.3.0", + "@angular/platform-browser-dynamic": "^13.3.0", + "@angular/platform-server": "^13.3.0", + "@angular/router": "^13.3.0", + "@microsoft/signalr": "^6.0.2", + "@popperjs/core": "^2.11.4", + "apexcharts": "^3.29.0", + "bootstrap": "^4.6.1", + "core-js": "^3.19.0", + "jquery": "^3.6.0", + "jwt-decode": "^3.1.2", + "ngx-toastr": "^14.1.4", + "oidc-client": "^1.11.5", + "reflect-metadata": "^0.1.13", + "rxjs": "^6.6.7", + "service": "^0.1.4", + "toastr": "^2.1.4", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^13.3.0", + "@angular/cli": "^13.3.0", + "@angular/compiler": "^13.3.0", + "@angular/compiler-cli": "^13.3.0", + "@angular/language-service": "^13.3.0", + "@types/jasmine": "~3.4.4", + "@types/jasminewd2": "^2.0.10", + "@types/node": "~12.11.6", + "codelyzer": "^6.0.2", + "jasmine-core": "^3.10.1", + "jasmine-spec-reporter": "~4.2.1", + "karma": "^6.3.7", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage-istanbul-reporter": "~2.1.0", + "karma-jasmine": "~2.0.1", + "karma-jasmine-html-reporter": "^1.7.0", + "typescript": "^4.5.5" + }, + "optionalDependencies": { + "protractor": "^7.0.0", + "ts-node": "~8.4.1", + "tslint": "~5.20.0" + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.js b/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.js new file mode 100644 index 00000000..ed5f0e1e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.js @@ -0,0 +1,83 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Dropdown = void 0; +var Dropdown = /** @class */ (function () { + function Dropdown(selectorOrElement) { + var _this = this; + this.visible = false; + if (typeof selectorOrElement === "string") { + this.menu = document.querySelector(selectorOrElement); + } + else if (selectorOrElement) { + this.menu = selectorOrElement; + } + else { + throw new Error("Not an element nor selector."); + } + window.addEventListener('resize', function () { + _this.reposition(); + }); + window.addEventListener('click', function (e) { + if (e.target === _this.menuTrigger) { + return; + } + if (!_this.isDescendant(_this.menu, e.target)) { + _this.hide(); + } + }); + } + Dropdown.prototype.bindClick = function (selectorOrElement) { + var _this = this; + if (typeof selectorOrElement === "string") { + this.menuTrigger = document.querySelector(selectorOrElement); + } + else if (selectorOrElement) { + this.menuTrigger = selectorOrElement; + } + else { + throw new Error("Not an element nor selector."); + } + this.menuTrigger.addEventListener('click', function (e) { + e.preventDefault(); + if (_this.visible) { + _this.hide(); + } + else { + _this.show(); + } + _this.visible = !_this.visible; + }); + }; + Dropdown.prototype.hide = function () { + this.menu.classList.remove('shown'); + }; + Dropdown.prototype.show = function () { + this.reposition(); + this.menu.classList.add('shown'); + }; + Dropdown.prototype.reposition = function () { + var triggerRect = this.menuTrigger.getBoundingClientRect(); + var menuRect = this.menu.getBoundingClientRect(); + if (triggerRect.left + menuRect.width + 10 > window.innerWidth) { + this.menu.style.left = (triggerRect.right - menuRect.width) + "px"; + this.menu.style.top = triggerRect.bottom + 5 + "px"; + } + else { + this.menu.style.left = triggerRect.left + "px"; + this.menu.style.top = triggerRect.bottom + 5 + "px"; + } + }; + Dropdown.prototype.isDescendant = function (parent, child) { + var node = child.parentNode; + while (node != null) { + if (node === parent) { + return true; + } + node = node.parentNode; + } + return false; + }; + return Dropdown; +}()); +exports.Dropdown = Dropdown; +//# sourceMappingURL=Dropdown.js.map \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.js.map b/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.js.map new file mode 100644 index 00000000..be4aa7e7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Dropdown.js","sourceRoot":"","sources":["Dropdown.ts"],"names":[],"mappings":";;;AAAA;IAKE,kBAAY,iBAAuC;QAAnD,iBAoBC;QAtBO,YAAO,GAAG,KAAK,CAAC;QAGtB,IAAI,OAAO,iBAAiB,KAAK,QAAQ,EAAE;YACzC,IAAI,CAAC,IAAI,GAAG,QAAQ,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;SACvD;aAAM,IAAI,iBAAiB,EAAE;YAC5B,IAAI,CAAC,IAAI,GAAgB,iBAAiB,CAAC;SAC5C;aAAM;YACL,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;SACjD;QAED,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE;YAChC,KAAI,CAAC,UAAU,EAAE,CAAC;QACpB,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,UAAA,CAAC;YAChC,IAAI,CAAC,CAAC,MAAM,KAAK,KAAI,CAAC,WAAW,EAAE;gBACjC,OAAO;aACR;YACD,IAAI,CAAC,KAAI,CAAC,YAAY,CAAC,KAAI,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE;gBAC3C,KAAI,CAAC,IAAI,EAAE,CAAC;aACb;QACH,CAAC,CAAC,CAAC;IACL,CAAC;IAED,4BAAS,GAAT,UAAU,iBAAuC;QAAjD,iBAkBC;QAjBC,IAAI,OAAO,iBAAiB,KAAK,QAAQ,EAAE;YACzC,IAAI,CAAC,WAAW,GAAG,QAAQ,CAAC,aAAa,CAAC,iBAAiB,CAAC,CAAC;SAC9D;aAAM,IAAI,iBAAiB,EAAE;YAC5B,IAAI,CAAC,WAAW,GAAgB,iBAAiB,CAAC;SACnD;aAAM;YACL,MAAM,IAAI,KAAK,CAAC,8BAA8B,CAAC,CAAC;SACjD;QAED,IAAI,CAAC,WAAW,CAAC,gBAAgB,CAAC,OAAO,EAAE,UAAA,CAAC;YAC1C,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,KAAI,CAAC,OAAO,EAAE;gBAChB,KAAI,CAAC,IAAI,EAAE,CAAC;aACb;iBAAM;gBACL,KAAI,CAAC,IAAI,EAAE,CAAC;aACb;YACD,KAAI,CAAC,OAAO,GAAG,CAAC,KAAI,CAAC,OAAO,CAAC;QAC/B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,uBAAI,GAAJ;QACE,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;IACtC,CAAC;IACD,uBAAI,GAAJ;QACE,IAAI,CAAC,UAAU,EAAE,CAAC;QAClB,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAED,6BAAU,GAAV;QACE,IAAI,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,qBAAqB,EAAE,CAAC;QAC3D,IAAI,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAC;QACjD,IAAI,WAAW,CAAC,IAAI,GAAG,QAAQ,CAAC,KAAK,GAAG,EAAE,GAAG,MAAM,CAAC,UAAU,EAAE;YAC9D,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;YACnE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;SACrD;aAAM;YACL,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,WAAW,CAAC,IAAI,GAAG,IAAI,CAAC;YAC/C,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,WAAW,CAAC,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC;SACrD;IACH,CAAC;IAGO,+BAAY,GAApB,UAAqB,MAAM,EAAE,KAAK;QAChC,IAAI,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC;QAC5B,OAAO,IAAI,IAAI,IAAI,EAAE;YACnB,IAAI,IAAI,KAAK,MAAM,EAAE;gBACnB,OAAO,IAAI,CAAC;aACb;YACD,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC;SACxB;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IACH,eAAC;AAAD,CAAC,AA9ED,IA8EC;AA9EY,4BAAQ"} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.ts new file mode 100644 index 00000000..add1bf8f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/Dropdown.ts @@ -0,0 +1,79 @@ +export class Dropdown { + private menu: HTMLElement; + private menuTrigger: HTMLElement; + private visible = false; + + constructor(selectorOrElement: string | HTMLElement) { + if (typeof selectorOrElement === "string") { + this.menu = document.querySelector(selectorOrElement); + } else if (selectorOrElement) { + this.menu = selectorOrElement; + } else { + throw new Error("Not an element nor selector."); + } + + window.addEventListener('resize', () => { + this.reposition(); + }); + window.addEventListener('click', e => { + if (e.target === this.menuTrigger) { + return; + } + if (!this.isDescendant(this.menu, e.target)) { + this.hide(); + } + }); + } + + bindClick(selectorOrElement: string | HTMLElement) { + if (typeof selectorOrElement === "string") { + this.menuTrigger = document.querySelector(selectorOrElement); + } else if (selectorOrElement) { + this.menuTrigger = selectorOrElement; + } else { + throw new Error("Not an element nor selector."); + } + + this.menuTrigger.addEventListener('click', e => { + e.preventDefault(); + if (this.visible) { + this.hide(); + } else { + this.show(); + } + this.visible = !this.visible; + }); + } + + hide() { + this.menu.classList.remove('shown'); + } + show() { + this.reposition(); + this.menu.classList.add('shown'); + } + + reposition() { + var triggerRect = this.menuTrigger.getBoundingClientRect(); + var menuRect = this.menu.getBoundingClientRect(); + if (triggerRect.left + menuRect.width + 10 > window.innerWidth) { + this.menu.style.left = (triggerRect.right - menuRect.width) + "px"; + this.menu.style.top = triggerRect.bottom + 5 + "px"; + } else { + this.menu.style.left = triggerRect.left + "px"; + this.menu.style.top = triggerRect.bottom + 5 + "px"; + } + } + + + private isDescendant(parent, child) { + let node = child.parentNode; + while (node != null) { + if (node === parent) { + return true; + } + node = node.parentNode; + } + return false; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.js b/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.js new file mode 100644 index 00000000..77b31f04 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.js @@ -0,0 +1,119 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Guide = void 0; +var Guide = /** @class */ (function () { + function Guide() { + var _this = this; + this.button = null; + this.guideContainer = null; + this.dimScreenElement = null; + this.closed = function () { }; + this.showNext = function () { }; + this.dimScreenElement = document.createElement("div"); + this.dimScreenElement.className = "dimScreen"; + this.guideContainer = document.getElementById("note"); + if (!this.guideContainer) { + throw new Error("Failed to find an element with id 'note'."); + } + var button = document.getElementById("guideCloseBtn"); + button.addEventListener("click", function () { + _this.reset(); + if (_this.closed) { + _this.closed(); + } + }); + this.nextButton = document.getElementById("guideNextBtn"); + this.nextButton.addEventListener('click', function (x) { return _this.onClickNext(); }); + } + /** + * Show a guide + * @param elementToHighlight Element which the guide is for. + * @param title Title in the guide + * @param body Guide body (i.e. the explanation). + * @param hasMore There are more guides available on this page. + */ + Guide.prototype.show = function (elementToHighlight, title, body, hasMore) { + if (!elementToHighlight) { + throw new Error("Element must be specified."); + } + if (!title) { + throw new Error("title must be specified."); + } + if (!body) { + throw new Error("body must be specified."); + } + if (this.elementToHighlight) { + this.reset(); + } + if (typeof elementToHighlight === "string") { + if (elementToHighlight.substr(0, 1) === "#") { + elementToHighlight = elementToHighlight.substr(1); + } + this.elementToHighlight = document.getElementById(elementToHighlight); + if (!this.elementToHighlight) { + throw new Error("Failed to find element " + elementToHighlight); + } + } + else { + this.elementToHighlight = elementToHighlight; + } + if (hasMore) { + this.nextButton.style.display = ''; + } + else { + this.nextButton.style.display = 'none'; + } + document.body.appendChild(this.dimScreenElement); + this.dimScreenElement.style.display = "block"; + this.guideContainer.style.display = "block"; + var elementRect = this.elementToHighlight.getBoundingClientRect(); + this.guideContainer.children[0].innerHTML = title; + var textStr = ""; + if ((typeof body === "string" && body.substr(0, 1) === '#')) { + textStr = document.getElementById(body.substr(1)).innerHTML; + } + else if (typeof body !== "string") { + textStr = body.innerHTML; + } + else { + textStr = body.replace(/(?:\r\n|\r|\n)/g, "
"); + } + this.guideContainer.children[1].innerHTML = textStr; + var note = this.guideContainer; + var noteRect = note.getBoundingClientRect(); + var bodyRect = document.body.getBoundingClientRect(); + if (noteRect.width + elementRect.right > bodyRect.right) { + note.style.left = (elementRect.right - noteRect.width) + "px"; + } + else { + note.style.left = elementRect.left + "px"; + } + if (noteRect.height + elementRect.bottom > bodyRect.bottom) { + note.style.top = (elementRect.top - noteRect.height - 10) + "px"; + } + else { + note.style.top = (elementRect.bottom + 10) + "px"; + } + this.highlight(); + }; + Guide.prototype.highlight = function () { + this.elementToHighlight.classList.add("guide-highlight"); + }; + Guide.prototype.reset = function () { + this.elementToHighlight.classList.remove("guide-highlight"); + this.elementToHighlight = null; + document.body.removeChild(this.dimScreenElement); + this.guideContainer.style.display = "none"; + this.guideContainer.style.top = ""; + this.guideContainer.style.bottom = ""; + this.guideContainer.style.left = ""; + this.guideContainer.style.right = ""; + this.dimScreenElement.style.display = "none"; + }; + Guide.prototype.onClickNext = function () { + this.showNext(); + }; + return Guide; +}()); +exports.Guide = Guide; +//# sourceMappingURL=Guide.js.map \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.js.map b/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.js.map new file mode 100644 index 00000000..0fcd28ac --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.js.map @@ -0,0 +1 @@ +{"version":3,"file":"Guide.js","sourceRoot":"","sources":["Guide.ts"],"names":[],"mappings":";;;AAAA;IAOE;QAAA,iBAoBC;QA1BD,WAAM,GAAgB,IAAI,CAAC;QAC3B,mBAAc,GAAgB,IAAI,CAAC;QACnC,qBAAgB,GAAgB,IAAI,CAAC;QA0BrC,WAAM,GAAG,cAAQ,CAAC,CAAC;QACnB,aAAQ,GAAG,cAAQ,CAAC,CAAC;QAtBnB,IAAI,CAAC,gBAAgB,GAAG,QAAQ,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACtD,IAAI,CAAC,gBAAgB,CAAC,SAAS,GAAG,WAAW,CAAC;QAE9C,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACtD,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE;YACxB,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;SAC9D;QACD,IAAM,MAAM,GAAG,QAAQ,CAAC,cAAc,CAAC,eAAe,CAAC,CAAC;QACxD,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAC7B;YACE,KAAI,CAAC,KAAK,EAAE,CAAC;YACb,IAAI,KAAI,CAAC,MAAM,EAAE;gBACf,KAAI,CAAC,MAAM,EAAE,CAAC;aACf;QACH,CAAC,CAAC,CAAC;QAGL,IAAI,CAAC,UAAU,GAAG,QAAQ,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC;QAC1D,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,OAAO,EAAE,UAAA,CAAC,IAAI,OAAA,KAAI,CAAC,WAAW,EAAE,EAAlB,CAAkB,CAAC,CAAC;IACrE,CAAC;IAKD;;;;;;OAMG;IACH,oBAAI,GAAJ,UAAK,kBAAwC,EAAE,KAAa,EAAE,IAA0B,EAAE,OAAgB;QACxG,IAAI,CAAC,kBAAkB,EAAE;YACvB,MAAM,IAAI,KAAK,CAAC,4BAA4B,CAAC,CAAC;SAC/C;QAED,IAAI,CAAC,KAAK,EAAE;YACV,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;SAC7C;QAED,IAAI,CAAC,IAAI,EAAE;YACT,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;SAC5C;QAED,IAAI,IAAI,CAAC,kBAAkB,EAAE;YAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;SACd;QAED,IAAI,OAAO,kBAAkB,KAAK,QAAQ,EAAE;YAC1C,IAAI,kBAAkB,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,GAAG,EAAE;gBAC3C,kBAAkB,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;aACnD;YAED,IAAI,CAAC,kBAAkB,GAAG,QAAQ,CAAC,cAAc,CAAC,kBAAkB,CAAC,CAAC;YACtE,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE;gBAC5B,MAAM,IAAI,KAAK,CAAC,4BAA0B,kBAAoB,CAAC,CAAC;aACjE;SACF;aAAM;YACL,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;SAC9C;QAED,IAAI,OAAO,EAAE;YACX,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,EAAE,CAAC;SACpC;aAAM;YACL,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;SACxC;QAED,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACjD,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;QAC9C,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,OAAO,GAAG,OAAO,CAAC;QAE5C,IAAM,WAAW,GAAG,IAAI,CAAC,kBAAkB,CAAC,qBAAqB,EAAE,CAAC;QACpE,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,KAAK,CAAC;QAElD,IAAI,OAAO,GAAG,EAAE,CAAC;QACjB,IAAI,CAAC,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE;YAC3D,OAAO,GAAG,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;SAC7D;aAAM,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE;YACnC,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC;SAC1B;aAAM;YACL,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;SACnD;QACD,IAAI,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,GAAG,OAAO,CAAC;QAEpD,IAAM,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC;QAEjC,IAAM,QAAQ,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAC9C,IAAM,QAAQ,GAAG,QAAQ,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAC;QAEvD,IAAI,QAAQ,CAAC,KAAK,GAAG,WAAW,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,EAAE;YACvD,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC;SAC/D;aAAM;YACL,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,WAAW,CAAC,IAAI,GAAG,IAAI,CAAC;SAC3C;QAED,IAAI,QAAQ,CAAC,MAAM,GAAG,WAAW,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE;YAC1D,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,GAAG,QAAQ,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC;SAClE;aAAM;YACL,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,WAAW,CAAC,MAAM,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC;SACnD;QACD,IAAI,CAAC,SAAS,EAAE,CAAC;IACnB,CAAC;IAEO,yBAAS,GAAjB;QACE,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC;IAC3D,CAAC;IAEO,qBAAK,GAAb;QACE,IAAI,CAAC,kBAAkB,CAAC,SAAS,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC;QAC5D,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAE/B,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACjD,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;QAC3C,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,GAAG,GAAG,EAAE,CAAC;QACnC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC;QACtC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,GAAG,EAAE,CAAC;QACpC,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,KAAK,GAAG,EAAE,CAAC;QACrC,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC;IAC/C,CAAC;IAEO,2BAAW,GAAnB;QACE,IAAI,CAAC,QAAQ,EAAE,CAAC;IAClB,CAAC;IACH,YAAC;AAAD,CAAC,AAnID,IAmIC;AAnIY,sBAAK"} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.ts new file mode 100644 index 00000000..ec069ec8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/Guide.ts @@ -0,0 +1,132 @@ +export class Guide { + button: HTMLElement = null; + guideContainer: HTMLElement = null; + dimScreenElement: HTMLElement = null; + elementToHighlight: HTMLElement; + private nextButton: HTMLElement; + + constructor() { + this.dimScreenElement = document.createElement("div"); + this.dimScreenElement.className = "dimScreen"; + + this.guideContainer = document.getElementById("note"); + if (!this.guideContainer) { + throw new Error("Failed to find an element with id 'note'."); + } + const button = document.getElementById("guideCloseBtn"); + button.addEventListener("click", + () => { + this.reset(); + if (this.closed) { + this.closed(); + } + }); + + + this.nextButton = document.getElementById("guideNextBtn"); + this.nextButton.addEventListener('click', x => this.onClickNext()); + } + + closed = () => { }; + showNext = () => { }; + + /** + * Show a guide + * @param elementToHighlight Element which the guide is for. + * @param title Title in the guide + * @param body Guide body (i.e. the explanation). + * @param hasMore There are more guides available on this page. + */ + show(elementToHighlight: HTMLElement | string, title: string, body: string | HTMLElement, hasMore: boolean) { + if (!elementToHighlight) { + throw new Error("Element must be specified."); + } + + if (!title) { + throw new Error("title must be specified."); + } + + if (!body) { + throw new Error("body must be specified."); + } + + if (this.elementToHighlight) { + this.reset(); + } + + if (typeof elementToHighlight === "string") { + if (elementToHighlight.substr(0, 1) === "#") { + elementToHighlight = elementToHighlight.substr(1); + } + + this.elementToHighlight = document.getElementById(elementToHighlight); + if (!this.elementToHighlight) { + throw new Error(`Failed to find element ${elementToHighlight}`); + } + } else { + this.elementToHighlight = elementToHighlight; + } + + if (hasMore) { + this.nextButton.style.display = ''; + } else { + this.nextButton.style.display = 'none'; + } + + document.body.appendChild(this.dimScreenElement); + this.dimScreenElement.style.display = "block"; + this.guideContainer.style.display = "block"; + + const elementRect = this.elementToHighlight.getBoundingClientRect(); + this.guideContainer.children[0].innerHTML = title; + + var textStr = ""; + if ((typeof body === "string" && body.substr(0, 1) === '#')) { + textStr = document.getElementById(body.substr(1)).innerHTML; + } else if (typeof body !== "string") { + textStr = body.innerHTML; + } else { + textStr = body.replace(/(?:\r\n|\r|\n)/g, "
"); + } + this.guideContainer.children[1].innerHTML = textStr; + + const note = this.guideContainer; + + const noteRect = note.getBoundingClientRect(); + const bodyRect = document.body.getBoundingClientRect(); + + if (noteRect.width + elementRect.right > bodyRect.right) { + note.style.left = (elementRect.right - noteRect.width) + "px"; + } else { + note.style.left = elementRect.left + "px"; + } + + if (noteRect.height + elementRect.bottom > bodyRect.bottom) { + note.style.top = (elementRect.top - noteRect.height - 10) + "px"; + } else { + note.style.top = (elementRect.bottom + 10) + "px"; + } + this.highlight(); + } + + private highlight() { + this.elementToHighlight.classList.add("guide-highlight"); + } + + private reset() { + this.elementToHighlight.classList.remove("guide-highlight"); + this.elementToHighlight = null; + + document.body.removeChild(this.dimScreenElement); + this.guideContainer.style.display = "none"; + this.guideContainer.style.top = ""; + this.guideContainer.style.bottom = ""; + this.guideContainer.style.left = ""; + this.guideContainer.style.right = ""; + this.dimScreenElement.style.display = "none"; + } + + private onClickNext() { + this.showNext(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/api-authorization.module.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/api-authorization.module.spec.ts new file mode 100644 index 00000000..f6a4b290 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/api-authorization.module.spec.ts @@ -0,0 +1,13 @@ +import { ApiAuthorizationModule } from './api-authorization.module'; + +describe('ApiAuthorizationModule', () => { + let apiAuthorizationModule: ApiAuthorizationModule; + + beforeEach(() => { + apiAuthorizationModule = new ApiAuthorizationModule(); + }); + + it('should create an instance', () => { + expect(apiAuthorizationModule).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/api-authorization.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/api-authorization.module.ts new file mode 100644 index 00000000..da729018 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/api-authorization.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { HttpClientModule } from '@angular/common/http'; + +@NgModule({ + imports: [ + CommonModule, + HttpClientModule + ], +}) +export class ApiAuthorizationModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.guard.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.guard.spec.ts new file mode 100644 index 00000000..4a4c3833 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.guard.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { AuthorizeGuard } from './authorize.guard'; + +describe('AuthorizeGuard', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AuthorizeGuard] + }); + }); + + it('should ...', inject([AuthorizeGuard], (guard: AuthorizeGuard) => { + expect(guard).toBeTruthy(); + })); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.guard.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.guard.ts new file mode 100644 index 00000000..dc90ffdd --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.guard.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@angular/core'; +import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from '@angular/router'; +import { Observable } from 'rxjs'; +import { AuthorizeService } from './authorize.service'; +declare var window: any; + +@Injectable({ + providedIn: 'root' +}) +export class AuthorizeGuard implements CanActivate { + constructor(private authorize: AuthorizeService, private router: Router) { + } + canActivate( + _next: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Observable | Promise | boolean { + + if (this.authorize.isAuthenticated()) { + return true; + } + if (AuthorizeGuard.isOpenAccountPage(state.url)) { + return true; + } + + var loginUrl = localStorage.getItem('loginUrl'); + if (loginUrl) { + window.location.href = loginUrl; + return false; + } + + //window.location = "/account/login"; + this.router.navigate(['account', 'login'], { + queryParams: { + 'returnUrl': state.url + } + }); + return false; + } + + static isOpenAccountPage(url: string): boolean { + console.log('checking url', url); + return url.indexOf('/account/') >= 0 && url.indexOf('account/settings') !== -1; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.interceptor.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.interceptor.spec.ts new file mode 100644 index 00000000..a97ac0a0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.interceptor.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { AuthorizeInterceptor } from './authorize.interceptor'; + +describe('AuthorizeInterceptor', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AuthorizeInterceptor] + }); + }); + + it('should be created', inject([AuthorizeInterceptor], (service: AuthorizeInterceptor) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.interceptor.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.interceptor.ts new file mode 100644 index 00000000..02c931df --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.interceptor.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@angular/core'; +import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from '@angular/common/http'; +import { Observable } from 'rxjs'; +import { AuthorizeService } from './authorize.service'; +import { mergeMap } from 'rxjs/operators'; + +@Injectable({ + providedIn: 'root' +}) +export class AuthorizeInterceptor implements HttpInterceptor { + constructor(private authorize: AuthorizeService) { } + + intercept(req: HttpRequest, next: HttpHandler): Observable> { + return this.authorize.getAccessToken2() + .pipe(mergeMap(token => this.processRequestWithToken(token, req, next))); + } + + // Checks if there is an access_token available in the authorize service + // and adds it to the request in case it's targeted at the same origin as the + // single page application. + private processRequestWithToken(token: string, req: HttpRequest, next: HttpHandler) { + if (!!token && this.isSameOriginUrl(req)) { + req = req.clone({ + setHeaders: { + Authorization: `Bearer ${token}` + } + }); + } + + return next.handle(req); + } + + private isSameOriginUrl(req: any) { + // It's an absolute url with the same origin. + if (req.url.startsWith(`${window.location.origin}/`)) { + return true; + } + + // It's a protocol relative url with the same origin. + // For example: //www.example.com/api/Products + if (req.url.startsWith(`//${window.location.host}/`)) { + return true; + } + + // It's a relative url like /api/Products + if (/^\/[^\/].*/.test(req.url)) { + return true; + } + + // It's an absolute or protocol relative url that + // doesn't have the same origin. + return false; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.service.spec.ts new file mode 100644 index 00000000..41c9d65e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { AuthorizeService } from './authorize.service'; + +describe('AuthorizeService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [AuthorizeService] + }); + }); + + it('should be created', inject([AuthorizeService], (service: AuthorizeService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.service.ts new file mode 100644 index 00000000..6aa764bf --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/api-authorization/authorize.service.ts @@ -0,0 +1,159 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Subscriber, Observable, Subject } from 'rxjs'; +import { HttpClient, IHttpResponse } from "../app/utils/HttpClient" +import jwt_decode from "jwt-decode"; + +//https://jasonwatmore.com/post/2019/05/17/angular-7-tutorial-part-4-login-form-authentication-service-route-guard +export enum AuthenticationResultStatus { + Success, + Redirect, + Fail +} + +interface Dictionary { + [key: string]: T; +} + +export interface IUser { + userName: string; + emailAddress: string; + accountId: number; + isSysAdmin: boolean; + role: string; + claims: Dictionary; +} + +@Injectable({ + providedIn: 'root' +}) +export class AuthorizeService { + private userSubject: BehaviorSubject = new BehaviorSubject(null); + private _token: string; + private subscriber: Subscriber = new Subscriber(); + private _user: IUser | null; + + + constructor(private service: HttpClient) { + var user = this.user; + this.userSubject.next(user); + } + + isAuthenticated(): boolean { + return this.getAccessToken() != null; + } + + get user(): IUser | null { + if (this._user == null) { + var json = localStorage.getItem('user'); + this._user = JSON.parse(json); + } + return this._user; + } + + get userEvents(): Observable { + return this.userSubject.asObservable(); + } + + public getAccessToken(): string { + if (!this._token) { + this._token = localStorage.getItem('jwt'); + } + + return this._token; + } + + public getAccessToken2(): Observable { + if (!this._token) { + this._token = localStorage.getItem('jwt'); + } + + return new BehaviorSubject(this._token); + } + + async logout(): Promise { + localStorage.removeItem('jwt'); + localStorage.removeItem('user'); + this._user = null; + this.userSubject.next(null); + return null; + } + + async login(userName: string, password: string): Promise { + var reply = await this.service.post("/api/account/login", JSON.stringify({ UserName: userName, Password: password })); + return this.processLoginReply(reply); + } + + async activate(activationCode: string): Promise { + var reply = await this.service.post("/api/account/activate/" + activationCode, null); + return this.processLoginReply(reply); + } + + getDecodedAccessToken(token: string): any { + try { + return jwt_decode(token); + } + catch (e) { + console.log(e); + return null; + } + } + + private processLoginReply(reply: IHttpResponse): IUser { + if (reply.statusCode !== 200) { + throw new Error(reply.statusReason); + } + if (!reply.body.success) { + throw new Error(reply.body.errorMessage); + } + + localStorage.setItem('jwt', reply.body.jwtToken); + + /* + application: 1 + application/admin: 1 + aud: "https://coderr.io" + exp: 1616162760 + iat: 1615557960 + iss: "https://coderr.io" + nameid: 1 + nbf: 1615557960 + role: "SysAdmin" + unique_name: "admin" + */ + let tokenInfo = this.getDecodedAccessToken(reply.body.jwtToken); + + var claims: Dictionary = {}; + for (var key in tokenInfo) { + if (!tokenInfo.hasOwnProperty(key)) { + continue; + } + switch (key) { + case "aud": + case "exp": + case "iat": + case "iss": + case "nbf": + case "unique_name": + break; + default: + claims[key] = tokenInfo[key]; + } + } + + var user: IUser = + { + accountId: tokenInfo.nameid, + claims: claims, + emailAddress: null, + isSysAdmin: tokenInfo.role === "SysAdmin", + role: tokenInfo.role, + userName: tokenInfo.unique_name + }; + + localStorage.setItem('user', JSON.stringify(user)); + this._user = user; + this.subscriber.next(this._token); + this.userSubject.next(user); + return user; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/PromiseWrapper.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/PromiseWrapper.ts new file mode 100644 index 00000000..ea27b22c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/PromiseWrapper.ts @@ -0,0 +1,68 @@ + +type MyHandler = () => Promise; + + +export class ExecuteOnce { + private _promise: Promise; + private acceptCallback: (value: T | PromiseLike) => void; + private rejectCallback: (reason?: any) => void; + private executed = false; + + constructor(private callback: MyHandler) { + this._promise = new Promise((accept, reject) => { + this.acceptCallback = accept; + this.rejectCallback = reject; + }); + } + + get promise(): Promise { + return this._promise; + } + + execute() { + if (this.executed) { + return; + } + this.executed = true; + + this.callback() + .then(x => this.acceptCallback(x)) + .catch(x => this.rejectCallback(x)); + } +} + +/** + * + */ +export class PromiseWrapper { + private _promise: Promise; + private acceptCallback: (value: T | PromiseLike) => void; + private rejectCallback: (reason?: any) => void; + private _completed: boolean; + + constructor() { + this._promise = new Promise((accept, reject) => { + this.acceptCallback = accept; + this.rejectCallback = reject; + }); + } + + get promise(): Promise { + return this._promise; + } + + get completed(): boolean { + return this._completed; + } + + accept(value: T) { + this._completed = true; + this.acceptCallback(value); + } + + reject(error: Error) { + this._completed = true; + this.rejectCallback(error); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/controls.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/controls.module.ts new file mode 100644 index 00000000..3a940575 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/controls.module.ts @@ -0,0 +1,11 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ModalComponent } from './modal/modal.component'; +import { GuideComponent } from "./guide/guide.component"; + +@NgModule({ + imports: [CommonModule], + declarations: [ModalComponent, GuideComponent], + exports: [ModalComponent, GuideComponent] +}) +export class ControlsModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.html new file mode 100644 index 00000000..d8e65af2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.html @@ -0,0 +1 @@ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.scss new file mode 100644 index 00000000..345a4e99 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.scss @@ -0,0 +1,5 @@ +@import "../../../styles/_partials/coderr-variables.scss"; + +.guide-content{ + display: none; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.ts new file mode 100644 index 00000000..62520c1f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.component.ts @@ -0,0 +1,56 @@ +import { Component, ViewEncapsulation, ElementRef, Input, OnInit, OnDestroy, ViewChild, AfterViewInit } from '@angular/core'; + +import { GuideService } from './guide.service'; + +@Component({ + selector: 'guide', + templateUrl: 'guide.component.html', + styleUrls: ['guide.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class GuideComponent implements OnInit, OnDestroy, AfterViewInit { + @Input() name: string; + private element: HTMLElement; + + constructor(private service: GuideService, private elemRef: ElementRef) { + } + + @ViewChild('child') textElement: ElementRef; + + ngOnInit(): void { + } + + ngAfterViewInit() { + if (!this.name) { + throw new Error('Guide must have a name.'); + } + + // to access the guideContent element. + this.element = this.elemRef.nativeElement.children[0]; + + if (this.element.children.length === 0) { + console.log(this.name + " does not have any children"); + } + + this.service.register(this); + } + + ngOnDestroy(): void { + this.service.unregister(this); + } + + get title(): string { + if (this.element.children.length > 1) { + return this.element.children[0].textContent; + } + return 'Guide'; + } + + get body(): HTMLElement { + if (this.element.children.length === 0) { + throw new Error("Expected " + this.name + " do have child content."); + } + + return this.element.children[1]; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.service.spec.ts new file mode 100644 index 00000000..61819ff7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { GuideService } from './guide.service'; + +describe('GuideService', () => { + let service: GuideService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GuideService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.service.ts new file mode 100644 index 00000000..b30c03b3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/guide/guide.service.ts @@ -0,0 +1,154 @@ +import { Injectable } from '@angular/core'; +import { Observable, Subject } from "rxjs"; +import { Router } from '@angular/router'; +import { SettingsService } from "../../utils/settings.service"; +import { ExecuteOnce } from "../../PromiseWrapper"; +import { Guide } from "../../../Guide"; +import { GuideComponent } from "./guide.component"; + +class GuideSettings { + constructor() { + this.shownGuides = []; + } + + /** Guides that have already been shown */ + shownGuides: string[]; + + hasShown(name: string): boolean { + return this.shownGuides.find(x => x === name) != null; + } +} + +@Injectable({ + providedIn: 'root' +}) +export class GuideService { + private guideManager = new Guide(); + private settings = new GuideSettings(); + private loadHandler = new ExecuteOnce(() => this.load()); + private availableGuides: GuideComponent[] = []; + private pageHaveGuides: Subject = new Subject(); + private activeGuide = ""; + private isLoaded=false; + + constructor( + private settingsService: SettingsService, + private router: Router) { + + if (!this.settingsService) { + throw new Error("Service was not included"); + } + + this.loadHandler.execute(); + this.guideManager.closed = () => this.guideClosed(); + this.guideManager.showNext = () => this.showNextGuide(); + } + + get guidesAvailable(): Observable { + return this.pageHaveGuides.asObservable(); + } + + /** + * Register a guide + * @param guideName Name, used to identify this specific guide. + */ + register(component: GuideComponent): void { + let hasShown = this.settings.hasShown(component.name); + if (hasShown) { + return; + } + + if (component.name === "showGuideTooltip" && this.isLoaded) { + this.show(component.name, component.title, component.body); + } + + this.availableGuides.push(component); + if (this.isLoaded && this.availableGuides.length === 1) { + this.pageHaveGuides.next(true); + } + } + + /** + * Remove a guide. + * @param guideName Name, used to identify this specific guide. + */ + unregister(component: GuideComponent) { + this.availableGuides = this.availableGuides.filter(x => x.name !== component.name); + if (this.availableGuides.length === 0) { + this.pageHaveGuides.next(false); + } + } + + showNextGuide() { + if (this.availableGuides.length === 0) { + this.pageHaveGuides.next(false); + return; + } + + var guide = this.availableGuides[0]; + this.show(guide.name, guide.title, guide.body); + } + + show(guideName: string, title: string, body: string | HTMLElement) { + if (!guideName) { + throw new Error("guideName must be specified."); + } + + if (!title) { + throw new Error("title must be specified for " + guideName); + } + + if (!body) { + throw new Error("body must be specified for " + guideName); + } + + + + this.activeGuide = guideName; + this.availableGuides = this.availableGuides.filter(x => x.name !== guideName); + + this.settings.shownGuides.push(guideName); + this.storeSettings(); + this.guideManager.show('#' + guideName, title, body, this.availableGuides.length > 0); + } + + async canShow(name: string): Promise { + await this.loadHandler.promise; + return this.activeGuide === "" && !this.settings.hasShown(name); + } + + async load(): Promise { + const settings = await this.settingsService.get("StoredGuides"); + if (settings) { + var config = JSON.parse(settings); + this.settings.shownGuides = config.shownGuides; + } + + // need to filter out shown as they got registered before this load. + this.availableGuides = this.availableGuides.filter(x => !this.settings.shownGuides.includes(x.name)); + this.pageHaveGuides.next(this.availableGuides.length > 0); + + if (this.canShow("showGuideTooltip")) { + var component = this.availableGuides.find(x => x.name === "showGuideTooltip"); + if (component) { + this.show(component.name, component.title, component.body); + } + } + + this.isLoaded = true; + return this.settings; + } + + private async guideClosed(): Promise { + if (this.availableGuides.length === 0) { + this.pageHaveGuides.next(false); + return; + } + this.activeGuide = null; + } + + private async storeSettings(): Promise { + const json = JSON.stringify(this.settings); + await this.settingsService.set("StoredGuides", json); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.html new file mode 100644 index 00000000..84a6b9f4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.html @@ -0,0 +1,8 @@ + + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.scss new file mode 100644 index 00000000..1c012907 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.scss @@ -0,0 +1,39 @@ +@import "../../../styles/_partials/coderr-variables.scss"; + +modal { + display: none; + + .modal { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1000; + overflow: auto; + min-width: 600px; + + .modal-body { + padding: 20px; + margin: 40px; + } + + .modal-body a.btn { + color: white; + } + } + + .modal-overlay { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: #000; + opacity: 0.75; + z-index: 900; + } +} + +body.modal-open { + overflow: hidden; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.ts new file mode 100644 index 00000000..e65b5344 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.component.ts @@ -0,0 +1,64 @@ +import { Component, ViewEncapsulation, ElementRef, Input, OnInit, OnDestroy } from '@angular/core'; + +import { ModalService } from './modal.service'; + +@Component({ + selector: 'modal', + templateUrl: 'modal.component.html', + styleUrls: ['modal.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class ModalComponent implements OnInit, OnDestroy { + @Input() id: string; + private element: any; + static activeIndices: number[] = []; + overlayIndex = 0; + modalIndex = 0; + + constructor(private modalService: ModalService, el: ElementRef) { + this.element = el.nativeElement; + + var max = 500; + ModalComponent.activeIndices.forEach(x => { + if (x > max) { + max = x; + } + }); + this.overlayIndex = max + 1; + this.modalIndex = max + 2; + ModalComponent.activeIndices.push(this.modalIndex); + } + + ngOnInit(): void { + if (!this.id) { + throw new Error('modal must have an id'); + } + + document.body.appendChild(this.element); + this.element.addEventListener('click', el => { + if (el.target.className === 'modal') { + this.close(); + } + }); + + this.modalService.add(this); + } + + ngOnDestroy(): void { + this.modalService.remove(this.id); + this.element.remove(); + } + + open(): void { + this.element.style.display = 'block'; + document.body.classList.add('modal-open'); + } + + // close modal + close(): void { + this.element.style.display = 'none'; + document.body.classList.remove('modal-open'); + + ModalComponent.activeIndices = ModalComponent.activeIndices.filter(x => x !== this.overlayIndex); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.service.ts new file mode 100644 index 00000000..789a1bc9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/_controls/modal/modal.service.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ModalService { + private modals: any[] = []; + + /** + * Invoked by the component itself and should not be called directly. + * @param modalComponent Component to add to the list + */ + add(modalComponent: any) { + this.modals.push(modalComponent); + } + + /** + * Invoked by the component itself and should not be called directly. + * @param modalComponent Component to remove from the list + */ + remove(id: string) { + this.modals = this.modals.filter(x => x.id !== id); + } + + /** + * Open a modal component + * @param id id of the modal + */ + open(id: string) { + const modal = this.modals.find(x => x.id === id); + modal.open(); + } + + /** + * Close a modal + * @param id id of the modal. + */ + close(id: string) { + const modal = this.modals.find(x => x.id === id); + modal.close(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.module.ts new file mode 100644 index 00000000..503176bd --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.module.ts @@ -0,0 +1,65 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { LoginComponent } from "./login.component"; +import { LogoutComponent } from "./logout.component"; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { RouterModule, Routes } from '@angular/router'; +import { SettingsComponent } from './settings/settings.component'; +import { NotificationsComponent } from './settings/notifications/notifications.component'; +import { ProfileComponent } from './settings/profile/profile.component'; +import { NavbarComponent } from './settings/navbar/navbar.component'; +import { ControlsModule } from "../_controls/controls.module"; +import { RegisterComponent } from "./register.component"; +import { ActivateComponent } from "./activate.component"; + +const routes: Routes = [ + { path: 'account/login', component: LoginComponent }, + { path: 'account/logout', component: LogoutComponent }, + { path: 'account/register', component: RegisterComponent }, + { path: 'account/activate/:activationCode', component: ActivateComponent }, + { + path: 'account/settings', + component: SettingsComponent, + children: [ + { + path: '', + component: ProfileComponent + }, + { + path: 'home', + component: ProfileComponent + }, + { + path: 'nots', + component: NotificationsComponent + } + ] + } +]; + +@NgModule({ + declarations: [ + LoginComponent, + LogoutComponent, + SettingsComponent, + NotificationsComponent, + ProfileComponent, + NavbarComponent, + RegisterComponent, + ActivateComponent + ], + imports: [ + ControlsModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + RouterModule.forChild(routes) + ], + exports: [ + CommonModule, + FormsModule, + ReactiveFormsModule, + RouterModule + ] +}) +export class AccountModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.service.spec.ts new file mode 100644 index 00000000..2ffad6f3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { AccountService } from './account.service'; + +describe('AccountService', () => { + let service: AccountService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(AccountService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.service.ts new file mode 100644 index 00000000..ef858fc1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/account.service.ts @@ -0,0 +1,107 @@ +import { Injectable } from '@angular/core'; +import { ApiClient } from "../utils/HttpClient"; +import { SignalRService } from "../services/signal-r.service"; +import { AuthorizeService } from "../../api-authorization/authorize.service"; +import * as api from "../../server-api/Core/Accounts"; + +export interface User { + id: number; + userName: string; + email: string; +} + +export interface IRegisterDTO { + UserName: string; + Password: string; + Password2: string; + Email: string; + FirstName?: string; + LastName?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class AccountService { + private users: User[] = []; + + constructor(private readonly apiClient: ApiClient, + private readonly signalR: SignalRService, + + private readonly authService: AuthorizeService) { } + + async getAllButMe(): Promise { + + if (this.users.length === 0) { + await this.loadUsers(); + } + + return this.users.filter(x => x.id !== this.authService.user.accountId); + } + + async getAll(): Promise { + if (this.users.length === 0) { + await this.loadUsers(); + } + + return this.users; + } + + /** + * + * @param userName + * @param password + * @param emailAddress + * @returns If activation is required. + */ + async register(userName: string, password: string, emailAddress: string): Promise { + var dto: IRegisterDTO = { + UserName: userName, + Password: password, + Password2: password, + Email: emailAddress + }; + var response = await fetch('/api/account/register', + { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: JSON.stringify(dto) + }); + + var json = await response.json(); + if (!json.success) { + throw new Error(json.errorMessage); + } + + return json.verificationIsRequested; + } + + async activate(activationCode: string) { + var response = await fetch('/api/account/activate/' + activationCode, + { + method: 'POST', + }); + + var json = await response.json(); + if (!json.success) { + throw new Error(json.errorMessage); + } + + + } + + private async loadUsers() { + const query = new api.ListAccounts(); + const result = await this.apiClient.query(query); + this.users = result.accounts.map(x => { + return { + id: x.accountId, + email: x.email, + userName: x.userName + }; + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/activate.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/activate.component.html new file mode 100644 index 00000000..669a3221 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/activate.component.html @@ -0,0 +1,13 @@ +
+
+
+

Activate account

+
+
+
+ {{errorMessage}} +
+
+ +
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/activate.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/activate.component.ts new file mode 100644 index 00000000..858ac640 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/activate.component.ts @@ -0,0 +1,38 @@ +import { Component } from '@angular/core'; +import { Router, ActivatedRoute } from '@angular/router'; +import { AuthorizeService } from "../../api-authorization/authorize.service"; +declare var window: any; + +@Component({ + selector: 'activate', + templateUrl: './activate.component.html' +}) +export class ActivateComponent { + errorMessage = ""; + failed = false; + activationCode = ""; + constructor( + private authService: AuthorizeService, + private router: Router, + activatedRoute: ActivatedRoute + ) { + + this.activationCode = activatedRoute.snapshot.params['activationCode']; + this.activate(); + } + + private async activate(): Promise { + if (!this.activationCode || this.activationCode === "") { + this.errorMessage = 'No activation code was specified'; + this.failed = true; + } + + try { + await this.authService.activate(this.activationCode); + this.router.navigate(['/']); + } catch (error) { + this.errorMessage = error.message; + this.failed = true; + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.html new file mode 100644 index 00000000..88df41b5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.html @@ -0,0 +1,37 @@ +
+ +
+
+

Login

+
+
+
+ +
+ + +
+ +
+ + +
+ + + + Register +
+ +
+ +
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.spec.ts new file mode 100644 index 00000000..b25b51e5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.spec.ts @@ -0,0 +1,36 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LoginComponent } from './login.component'; + +describe('CounterComponent', () => { + let component: LoginComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [LoginComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LoginComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should display a title', async(() => { + const titleText = fixture.nativeElement.querySelector('h1').textContent; + expect(titleText).toEqual('Counter'); + })); + + it('should start with count 0, then increments by 1 when clicked', async(() => { + const countElement = fixture.nativeElement.querySelector('strong'); + expect(countElement.textContent).toEqual('0'); + + const incrementButton = fixture.nativeElement.querySelector('button'); + incrementButton.click(); + fixture.detectChanges(); + expect(countElement.textContent).toEqual('1'); + })); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.ts new file mode 100644 index 00000000..d033808b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/login.component.ts @@ -0,0 +1,42 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { AuthorizeService } from "../../api-authorization/authorize.service"; + +@Component({ + selector: 'login', + templateUrl: './login.component.html' + //styleUrls: ['./cart.component.css'] +}) +export class LoginComponent { + loginForm = this.formBuilder.group({ + userName: '', + password: '' + }); + errorMessage = ''; + returnUrl = ''; + + constructor( + private authService: AuthorizeService, + private formBuilder: FormBuilder, + private router: Router + ) { + } + + onSubmit(): void { + + this.authService.login(this.loginForm.value.userName, this.loginForm.value.password) + .then(x => { + this.loginForm.reset(); + var loginUrl = localStorage.getItem('loginUrl'); + if (loginUrl && this.returnUrl.replace(/\/$/, '') === loginUrl.replace(/\/$/, '')) { + this.router.navigate(['/']); + } else { + this.router.navigate([this.returnUrl]); + } + }).catch(e => { + this.errorMessage = e.message.replace(/\n/g, "
\n"); + }); + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/logout.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/logout.component.html new file mode 100644 index 00000000..f802f748 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/logout.component.html @@ -0,0 +1 @@ +You have been logged out. diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/logout.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/logout.component.ts new file mode 100644 index 00000000..0a566c4c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/logout.component.ts @@ -0,0 +1,23 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthorizeService } from "../../api-authorization/authorize.service"; +declare var window: any; + +@Component({ + selector: 'logout', + templateUrl: './logout.component.html' +}) +export class LogoutComponent { + constructor( + private authService: AuthorizeService, + private router: Router + ) { + this.authService.logout(); + var loginUrl = localStorage.getItem('loginUrl'); + if (loginUrl) { + window.location.href = loginUrl; + } else { + this.router.navigate(['account', 'login']); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/register.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/register.component.html new file mode 100644 index 00000000..b9e032cd --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/register.component.html @@ -0,0 +1,60 @@ +
+ +
+
+

Register

+
+
+
+
+ + +
+ +
+ + + {{strengthMessage}} +
+ +
+ + +
+ +
+ + +
+
+ +
+
+ + +
+

+ + Back to login +

+
+ +
+ +
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/register.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/register.component.ts new file mode 100644 index 00000000..4e6f5d29 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/register.component.ts @@ -0,0 +1,121 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { FormBuilder, Validators } from '@angular/forms'; +import { AccountService } from "./account.service"; +import { AuthorizeService } from "../../api-authorization/authorize.service"; + +@Component({ + selector: 'register', + templateUrl: './register.component.html' + //styleUrls: ['./register.component.css'] +}) +export class RegisterComponent implements OnInit { + regForm = this.formBuilder.group({ + userName: ['', Validators.required], + password: ['', Validators.required], + password2: ['', Validators.required], + emailAddress: ['', [Validators.required, Validators.email]], + }); + errorMessage = ''; + returnUrl = ''; + loginUrl = ''; + strengthMessage = ''; + activationRequired = false; + + constructor( + private accountService: AccountService, + private authService: AuthorizeService, + private formBuilder: FormBuilder, + private router: Router + ) { + this.loginUrl = localStorage.getItem('loginUrl'); + } + + ngOnInit(): void { + + } + + saveForm(): void { + var errors = []; + this.validatePassword(errors); + + if (errors.length > 0) { + this.errorMessage = errors.join('
'); + return; + } + + this.register(); + } + + checkPassword(text: string) { + this.strengthMessage = this.checkPassStrength(text); + } + + private async register(): Promise { + + try { + var activationRequired = await this.accountService.register(this.regForm.value.userName, + this.regForm.value.password, + this.regForm.value.emailAddress); + + if (!activationRequired) { + await this.authService.login(this.regForm.value.userName, this.regForm.value.password); + this.router.navigate(['/']); + } else { + this.activationRequired = true; + } + + } catch (e) { + this.errorMessage = e.message.replace(/\n/g, "
\n"); + } + } + + private validatePassword(errors: string[]) { + if (this.regForm.value.password !== this.regForm.value.password2) { + errors.push('Entered passwords do not match.'); + } + } + + private scorePassword(pass: string): number { + let score = 0; + if (!pass) + return score; + + // award every unique letter until 5 repetitions + const letters = new Object(); + for (let i = 0; i < pass.length; i++) { + letters[pass[i]] = (letters[pass[i]] || 0) + 1; + score += 5.0 / letters[pass[i]]; + } + + // bonus points for mixing it up + var variations = { + digits: /\d/.test(pass), + lower: /[a-z]/.test(pass), + upper: /[A-Z]/.test(pass), + nonWords: /\W/.test(pass), + }; + + let variationCount = 0; + for (let check in variations) { + variationCount += (variations[check] === true) ? 1 : 0; + } + score += (variationCount - 1) * 10; + + return score; + } + + private checkPassStrength(pass: string) { + const score = this.scorePassword(pass); + if (score > 80) + return "You got a new high score!"; + if (score > 50) + return "Good enough, but you can do better.."; + if (score >= 20) + return "Weak password"; + if (score < 20) + return "'1234' is not a real password..."; + return ""; + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.html new file mode 100644 index 00000000..c2d8872c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.html @@ -0,0 +1,16 @@ + + + +

Notifications

+

The notifications are found here ;)

+
+ diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.scss new file mode 100644 index 00000000..71899606 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.scss @@ -0,0 +1,33 @@ +@import "../../../../styles/_partials/coderr-variables.scss"; +@import "../../../../styles/_partials/_mixins.scss"; + +.submenu { + color: #ddd; + background: $nav-sub-bg; + padding-left: 15px; + padding-right: 15px; + + + .actions { + margin-left: auto; + margin-bottom: 3px; + margin-top: auto; + + a { + color: darken($blue, 50%); + padding: 3px 5px; +/* border-top: 1px solid darken($blue, 20%); +*/ border-left: 2px solid darken($blue, 15%); + /* border-right: 1px solid darken($blue, 20%);*/ + background-color: $blue; + + &.active { + color: lighten($blue, 50%); + } + + &:hover { + color: $red; + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.spec.ts new file mode 100644 index 00000000..f8ccd6f4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NavbarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.ts new file mode 100644 index 00000000..0c4d9582 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/navbar/navbar.component.ts @@ -0,0 +1,24 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { ModalService } from "../../../_controls/modal/modal.service"; +import { ToastrService } from "ngx-toastr"; +import { AccountService, User } from "../../../accounts/account.service"; + +@Component({ + selector: 'account-settings-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'] +}) +export class NavbarComponent implements OnInit { + + constructor( + private modalService: ModalService, + private toastrService: ToastrService, + private accountService: AccountService + ) { + } + + ngOnInit(): void { + } + + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.html new file mode 100644 index 00000000..f79883d3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.html @@ -0,0 +1,164 @@ +
+
+
+

Notifications

+
+

+ Enable notifications to get notified directly when something important happens. +

+
+ You have denied push notifications from Coderr. Edit the notification settings in your browser, and click save again. +
+
+ Look for the notification request in your browser (prompt or icon in the address bar). It can be an icon in the address bar or similar. You must accept notifications for this to work. +
+
+
+
+
+
+
+

Escalation notifications

+
+
New error
+
+

A new error have been received (only active for errors in your production environments).

+
+ +
+
+
+ Important error +
+
+

An existing error is escalated to important. Learn more.

+
+ +
+
+ +
Critical error
+
+

An existing error is escalated from important to critical. Learn more.

+
+ +
+ +
+
+
+
+

Standard notifications

+
+
Report spike
+
+

An application gets an unusual high volume of error reports (over the 85th percentile).

+
+ + +
+ + +
+
Re-opened error
+
+

A closed error is reopened (an report is received in a newer application version than the one that you closed the error for).

+
+ + +
+ +
+
New user feedback
+
+

An user have written an error report for an error.

+
+ +
+
+
+
+
+ +
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.scss new file mode 100644 index 00000000..fb161ddc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.scss @@ -0,0 +1,17 @@ +.card-group { +/* display: flex; + flex-wrap: wrap; +*/} + +.card { + background: white; + margin: 1px; + padding: 5px; + min-width: 500px; + .card-header{ + display: block; + font-size: 1.3em; + font-weight: bold; + margin-bottom: 10px; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.spec.ts new file mode 100644 index 00000000..4ae22edc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NotificationsComponent } from './notifications.component'; + +describe('NotificationsComponent', () => { + let component: NotificationsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NotificationsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NotificationsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.ts new file mode 100644 index 00000000..73a8c78b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/notifications/notifications.component.ts @@ -0,0 +1,219 @@ +import { Component, OnInit } from '@angular/core'; +import { ApiClient, HttpClient } from "../../../utils/HttpClient"; +import * as dto from "../../../../server-api/Core/Users"; +import { ToastrService } from "ngx-toastr"; +import { AuthorizeService } from "../../../../api-authorization/authorize.service"; + +@Component({ + selector: 'app-notifications', + templateUrl: './notifications.component.html', + styleUrls: ['./notifications.component.scss'] +}) +export class NotificationsComponent implements OnInit { + applicationId: number | null = null; + appNotice: string; + private workerUrl = '/service-worker.js'; + settings: dto.UpdateNotifications = new dto.UpdateNotifications(); + emailAddress = ''; + workEmail = ''; + missingKeys = false; + generatedPublicKey = ''; + generatedPrivateKey = ''; + deniedPushNotification = false; + pushNotificationRequest = false; + + constructor(private apiClient: ApiClient, + private httpClient: HttpClient, + private authService: AuthorizeService, + private toastrService: ToastrService) { + this.settings.notifyOnNewIncidents = dto.NotificationState.Disabled; + this.settings.notifyOnCriticalIncidents = dto.NotificationState.Disabled; + this.settings.notifyOnImportantIncidents = dto.NotificationState.Disabled; + this.settings.notifyOnPeaks = dto.NotificationState.Disabled; + this.settings.notifyOnReOpenedIncident = dto.NotificationState.Disabled; + this.settings.notifyOnUserFeedback = dto.NotificationState.Disabled; + } + + ngOnInit(): void { + this.loadSettings(); + } + + + private async loadSettings(): Promise { + var query = new dto.GetUserSettings(); + + var x = await this.apiClient.query(query) + this.settings.notifyOnImportantIncidents = x.notifications.notifyOnImportantIncidents; + this.settings.notifyOnCriticalIncidents = x.notifications.notifyOnCriticalIncidents; + this.settings.notifyOnNewIncidents = x.notifications.notifyOnNewIncidents; + this.settings.notifyOnPeaks = x.notifications.notifyOnPeaks; + this.settings.notifyOnUserFeedback = x.notifications.notifyOnUserFeedback; + this.settings.notifyOnReOpenedIncident = x.notifications.notifyOnReOpenedIncident; + this.emailAddress = x.emailAddress; + this.workEmail = x.emailAddress; + + navigator.serviceWorker.ready.then(registration => { + registration.pushManager.getSubscription(); + }); + + this.getPublicKey().then(key => { + if (key) { + return null; + } + }); + + return null; + } + + async save() { + var cmd = new dto.UpdateNotifications(); + cmd.notifyOnCriticalIncidents = +this.settings.notifyOnCriticalIncidents; + cmd.notifyOnImportantIncidents = +this.settings.notifyOnImportantIncidents; + cmd.notifyOnNewIncidents = +this.settings.notifyOnNewIncidents; + cmd.notifyOnPeaks = +this.settings.notifyOnPeaks; + cmd.notifyOnReOpenedIncident = +this.settings.notifyOnReOpenedIncident; + cmd.notifyOnUserFeedback = +this.settings.notifyOnUserFeedback; + cmd.applicationId = this.applicationId; + cmd.userId = this.authService.user.accountId; + await this.apiClient.command(cmd); + + var browserNotificationState = dto.NotificationState.BrowserNotification; + var shouldGenerateSubscription = cmd.notifyOnNewIncidents === browserNotificationState || + cmd.notifyOnPeaks === browserNotificationState || + cmd.notifyOnCriticalIncidents === browserNotificationState || + cmd.notifyOnImportantIncidents === browserNotificationState || + cmd.notifyOnReOpenedIncident === browserNotificationState || + cmd.notifyOnUserFeedback === browserNotificationState; + + if (shouldGenerateSubscription) { + if (!this.isNotificationsSupported()) { + this.toastrService.warning("Your browser do not support notifications"); + return null; + } + + this.registerPushSubscription().then(result => { + if (result) { + this.toastrService.success('Saved OK'); + } + }); + } else { + this.deleteSubscription(); + this.toastrService.success('Saved OK'); + } + + + } + + private async deleteSubscription() { + if (!navigator.serviceWorker) + return; + + const registration = await navigator.serviceWorker.ready; + const subscription = await registration.pushManager.getSubscription(); + if (!subscription) { + return; + } + + subscription.unsubscribe(); + + var cmd = new dto.DeleteBrowserSubscription(); + cmd.endpoint = subscription.endpoint; + cmd.userId = this.authService.user.accountId; + this.apiClient.command(cmd); + } + + + isNotificationsSupported() { + if (!('serviceWorker' in navigator)) { + return false; + } + + if (!('PushManager' in window)) { + return false; + } + + return true; + } + + private async registerPushSubscription(): Promise { + if (Notification.permission !== 'granted') { + try { + this.pushNotificationRequest = true; + const permission = await Notification.requestPermission(); + this.pushNotificationRequest = false; + if (permission !== 'granted') { + this.deniedPushNotification = true; + return false; + } + } catch (e) { + this.pushNotificationRequest = false; + this.deniedPushNotification = true; + console.log(e); + return false; + } + } + + await navigator.serviceWorker.register(this.workerUrl); + const registration = await navigator.serviceWorker.ready; + let subscription = await registration.pushManager.getSubscription(); + if (subscription) { + this.storeBrowserSubscription(subscription); + return true; + } + + const key = await this.getPublicKey(); + + subscription = await registration.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: key + }); + + this.storeBrowserSubscription(subscription); + return true; + } + + private async storeBrowserSubscription(subscription: PushSubscription) { + + var mightHave = subscription; + var cmd = new dto.StoreBrowserSubscription(); + cmd.userId = this.authService.user.accountId; + cmd.endpoint = subscription.endpoint; + cmd.expirationTime = mightHave.expirationTime; + + // Know a better way which isn't allocate a lot of objects? + var obj = JSON.parse(JSON.stringify(subscription)); + + cmd.publicKey = obj.keys.p256dh; + cmd.authenticationSecret = obj.keys.auth; + this.apiClient.command(cmd); + } + + private async getPublicKey(): Promise { + + const response = await this.httpClient.get('./push/vapidpublickey'); + if (response.statusCode === 204) { + return null; + } + + const json = await response.body; + var value = this.urlBase64ToUint8Array(json); + return value; + } + + private urlBase64ToUint8Array(base64String: string) { + var padding = '='.repeat((4 - base64String.length % 4) % 4); + var base64 = (base64String + padding) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + var rawData = window.atob(base64); + var outputArray = new Uint8Array(rawData.length); + + for (var i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + + return outputArray; + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.html new file mode 100644 index 00000000..7b2fb2ac --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.html @@ -0,0 +1,41 @@ +
+
+

Account settings

+
+
+
+ +
+ + +
+ + +
+ + +
+ + +
+ + +

Must be the same email address as in the work planning system (Jira, Azure DevOps) etc for the integration to work properly

+
+ +
+ {{errorMessage}} + +
+ +
+ +
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.scss new file mode 100644 index 00000000..b046505d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.scss @@ -0,0 +1,3 @@ +.form-group{ + margin-bottom: 10px; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.spec.ts new file mode 100644 index 00000000..e88012e7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProfileComponent } from './profile.component'; + +describe('ProfileComponent', () => { + let component: ProfileComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ProfileComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ProfileComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.ts new file mode 100644 index 00000000..b182fcbc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/profile/profile.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit } from '@angular/core'; +import { ApiClient } from "../../../utils/HttpClient"; +import { ToastrService } from "ngx-toastr"; +import { FormBuilder } from '@angular/forms'; +import * as dto from "../../../../server-api/Core/Users"; + +@Component({ + selector: 'app-profile', + templateUrl: './profile.component.html', + styleUrls: ['./profile.component.scss'] +}) +export class ProfileComponent implements OnInit { + form = this.formBuilder.group({ + workEmail: '', + firstName: '', + lastName: '' + }); + errorMessage = ''; + constructor(private apiClient: ApiClient, + private formBuilder: FormBuilder, + private toastrService: ToastrService) { } + + ngOnInit(): void { + var query = new dto.GetUserSettings(); + + this.apiClient.query(query) + .then(x => { + this.form.get("workEmail").setValue(x.emailAddress); + this.form.get("firstName").setValue(x.firstName); + this.form.get("lastName").setValue(x.lastName); + }); + } + + onSubmit(): void { + this.saveSettings(); + } + + async saveSettings() { + var cmd = new dto.UpdatePersonalSettings(); + cmd.emailAddress = this.form.value.workEmail; + cmd.firstName = this.form.value.firstName; + cmd.lastName = this.form.value.lastName; + await this.apiClient.command(cmd); + this.toastrService.success('Saved OK'); + + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.html new file mode 100644 index 00000000..80dea3a3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.html @@ -0,0 +1,3 @@ + + + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.spec.ts new file mode 100644 index 00000000..a3a508b0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SettingsComponent } from './settings.component'; + +describe('SettingsComponent', () => { + let component: SettingsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SettingsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.ts new file mode 100644 index 00000000..37f477fb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/accounts/settings/settings.component.ts @@ -0,0 +1,18 @@ +import { Component, OnInit } from '@angular/core'; +import { NavMenuService } from "../../nav-menu/nav-menu.service"; + +@Component({ + selector: 'app-settings', + templateUrl: './settings.component.html', + styleUrls: ['./settings.component.scss'] +}) +export class SettingsComponent implements OnInit { + + constructor(menuService: NavMenuService) { + menuService.updateNav([{id: null, route: null, title: 'Account settings'}]); + } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.html new file mode 100644 index 00000000..00269f1c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.html @@ -0,0 +1,4 @@ +
+

Administration area

+

Click on one of the links top right to access the different administration areas.

+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.spec.ts new file mode 100644 index 00000000..8348f63c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminMainComponent } from './admin-main.component'; + +describe('AdminMainComponent', () => { + let component: AdminMainComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AdminMainComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminMainComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.ts new file mode 100644 index 00000000..42c8e066 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin-main/admin-main.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit } from '@angular/core'; +import { NavMenuService } from "../../nav-menu/nav-menu.service"; + +@Component({ + selector: 'app-admin-main', + templateUrl: './admin-main.component.html', + styleUrls: ['./admin-main.component.scss'] +}) +export class AdminMainComponent implements OnInit { + + constructor( + navMenuService: NavMenuService) { + navMenuService.updateNav([ + { title: 'System Administration', route: ['/admin'] } + ]); + } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.html new file mode 100644 index 00000000..a798e658 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.html @@ -0,0 +1,2 @@ + + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.spec.ts new file mode 100644 index 00000000..6788c957 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.spec.ts @@ -0,0 +1,36 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminComponent } from './admin.component'; + +describe('CounterComponent', () => { + let component: AdminComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [AdminComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should display a title', async(() => { + const titleText = fixture.nativeElement.querySelector('h1').textContent; + expect(titleText).toEqual('Counter'); + })); + + it('should start with count 0, then increments by 1 when clicked', async(() => { + const countElement = fixture.nativeElement.querySelector('strong'); + expect(countElement.textContent).toEqual('0'); + + const incrementButton = fixture.nativeElement.querySelector('button'); + incrementButton.click(); + fixture.detectChanges(); + expect(countElement.textContent).toEqual('1'); + })); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.ts new file mode 100644 index 00000000..123a5412 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.component.ts @@ -0,0 +1,49 @@ +import { Component } from '@angular/core'; +import { Router, Route } from '@angular/router'; +import { FormsModule, ReactiveFormsModule, FormBuilder } from '@angular/forms'; +import { AuthorizeService } from "../../api-authorization/authorize.service"; + +@Component({ + selector: 'admin-main', + templateUrl: './admin.component.html' + //styleUrls: ['./admin.component.css'] +}) +export class AdminComponent { + loginForm = this.formBuilder.group({ + userName: '', + password: '' + }); + errorMessage = ''; + returnUrl = ''; + + constructor( + private authService: AuthorizeService, + private formBuilder: FormBuilder, + private router: Router + ) { + //this.printpath('', this.router.config); + + } + + printpath(parent: String, config: Route[]) { + for (let i = 0; i < config.length; i++) { + const route = config[i]; + if (route.children) { + const currentPath = route.path ? parent + '/' + route.path : parent; + this.printpath(currentPath, route.children); + } + } + } + + onSubmit(values): void { + + this.authService.login(this.loginForm.value.userName, this.loginForm.value.password) + .then(x => { + this.loginForm.reset(); + this.router.navigate([this.returnUrl]); + }).catch(e => { + this.errorMessage = e.message.replace(/\n/g, "
\n"); + }); + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.module.ts new file mode 100644 index 00000000..1e727522 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/admin.module.ts @@ -0,0 +1,67 @@ +import { NgModule } from "@angular/core"; +import { RouterModule, Routes } from "@angular/router"; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { CommonModule } from "@angular/common"; + +import { AdminMainComponent } from "./admin-main/admin-main.component"; +import { CreateApplicationComponent } from "./app-create/create.component"; +import { AdminComponent } from "./admin.component"; +import { ApiKeyListComponent } from "./apikeys/list/list.component"; +import { EditApiKeyComponent } from "./apikeys/edit/edit.component"; +import { CreateApiKeyComponent } from "./apikeys/create/create.component"; +import { ApiKeyDetailsComponent } from "./apikeys/details/details.component"; +import { AdminNavbarComponent } from "./navbar/navbar.component"; + +import { ControlsModule } from "../_controls/controls.module"; +import { WhitelistModule, whitelistRoutes } from "./whitelist/whitelist.module"; +import { GroupModule, groupRoutes } from "./groups/group.module"; + +var ourRoutes: Routes = [ + { + path: "admin", + component: AdminComponent, + children: [ + { path: "", component: AdminMainComponent }, + { path: "application", component: CreateApplicationComponent }, + { path: "apikeys", component: ApiKeyListComponent }, + { path: "apikeys/new", component: CreateApiKeyComponent }, + { path: "apikeys/:id/edit", component: EditApiKeyComponent }, + { path: "apikeys/:id", component: ApiKeyDetailsComponent }, + { path: "teams", component: ApiKeyListComponent }, + { path: "teams/team", component: CreateApiKeyComponent }, + { path: "teams/team/:id/edit", component: EditApiKeyComponent }, + { path: "teams/team/:id", component: ApiKeyDetailsComponent } + ] + } +]; +ourRoutes[0].children.push.apply(ourRoutes[0].children, whitelistRoutes); +ourRoutes[0].children.push.apply(ourRoutes[0].children, groupRoutes); + +@NgModule({ + declarations: [ + AdminComponent, + AdminMainComponent, + CreateApplicationComponent, + ApiKeyListComponent, + EditApiKeyComponent, + CreateApiKeyComponent, + ApiKeyDetailsComponent, + AdminNavbarComponent + ], + imports: [ + ControlsModule, + CommonModule, + FormsModule, + WhitelistModule, + GroupModule, + ReactiveFormsModule, + RouterModule.forChild(ourRoutes) + ], + exports: [ + CreateApplicationComponent, + RouterModule + ] +}) +export class AdminModule { + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-key.model.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-key.model.spec.ts new file mode 100644 index 00000000..3b703a16 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-key.model.spec.ts @@ -0,0 +1,7 @@ +import { ApiKey } from './api-key.model'; + +describe('ApiKey', () => { + it('should create an instance', () => { + expect(new ApiKey()).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-key.model.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-key.model.ts new file mode 100644 index 00000000..0e10789e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-key.model.ts @@ -0,0 +1,18 @@ +export interface IApplicationSummary { + id: number; + name: string; +} +export class ApiKey { + id: number; + title: string; + applications: IApplicationSummary[] = []; + accountId: number; + apiKey: string; + sharedSecret: string; +} + +export interface IApiKeyListItem { + id: number; + title: string; + apiKey: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-keys.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-keys.service.spec.ts new file mode 100644 index 00000000..fa8d52b2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-keys.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ApiKeyService } from './api-keys.service'; + +describe('ApiKeysServiceService', () => { + let service: ApiKeyService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ApiKeyService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-keys.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-keys.service.ts new file mode 100644 index 00000000..c2d53b6f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/api-keys.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@angular/core'; +import { ApiClient } from "../../utils/HttpClient"; +import * as api from "../../../server-api/Core/ApiKeys"; +import { ApiKey, IApiKeyListItem } from "./api-key.model"; + +@Injectable({ + providedIn: 'root' +}) +export class ApiKeyService { + + constructor(private apiClient: ApiClient) { + } + + async create(apiKey: ApiKey): Promise { + if (!apiKey) { + throw new Error("Key must be specified."); + } + if (apiKey.id) { + throw new Error("Key.id must not be specified."); + } + + var cmd = new api.CreateApiKey(); + cmd.applicationName = apiKey.title; + cmd.applicationIds = apiKey.applications.map(x => x.id); + cmd.accountId = apiKey.accountId; + cmd.apiKey = apiKey.apiKey; + cmd.sharedSecret = apiKey.sharedSecret; + await this.apiClient.command(cmd); + } + + async update(apiKey: ApiKey): Promise { + if (!apiKey) { + throw new Error("Key must be specified."); + } + if (!apiKey.id) { + throw new Error("Key.id must be specified."); + } + + var cmd = new api.EditApiKey(); + cmd.applicationName = apiKey.title; + cmd.applicationIds = apiKey.applications.map(x => x.id); + cmd.id = apiKey.id; + await this.apiClient.command(cmd); + } + + async get(id: number): Promise { + if (!id) { + throw new Error("id must be specified."); + } + + var query = new api.GetApiKey(); + query.id = id; + var result = await this.apiClient.query(query); + + var key = new ApiKey(); + key.id = id; + key.title = result.applicationName; + key.applications = result.allowedApplications.map(x => { + return { id: x.applicationId, name: x.applicationName }; + }); + + key.accountId = result.createdById; + key.apiKey = result.generatedKey; + key.sharedSecret = result.sharedSecret; + return key; + } + + async list(): Promise { + + var query = new api.ListApiKeys(); + var result = await this.apiClient.query(query); + return result.keys.map(x => { + var key: IApiKeyListItem = { + apiKey: x.apiKey, + id: x.id, + title: x.applicationName + }; + return key; + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.html new file mode 100644 index 00000000..0a6dc624 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.html @@ -0,0 +1,41 @@ +
+
+

Create an api key

+
+
+
+
+
+ + + Name describing the usage of this API key. +
+
+
+ +
+
+

Accessible applications

+

Applications that they API key are allowed to control. Leave all check boxes unchecked to allow all applications.

+
+ + +
+
+ You can use the Coderr.Server.Api.Client nuget package to communicate using this key, or web sockets. +
+
+ +
+ +
+ +
+ + +
+ diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.spec.ts new file mode 100644 index 00000000..4cbd00f8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateApiKeyComponent } from './create.component'; + +describe('CreateComponent', () => { + let component: CreateApiKeyComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CreateApiKeyComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateApiKeyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.ts new file mode 100644 index 00000000..1d1ed06d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/create/create.component.ts @@ -0,0 +1,95 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router, ActivatedRoute } from "@angular/router"; +import { ApplicationService } from "../../../applications/application.service"; +import { IApplication } from "../../../applications/application.model"; +import { AuthorizeService } from "../../../../api-authorization/authorize.service"; +import { ToastrService } from "ngx-toastr"; +import { ApiKeyService } from "../api-keys.service"; +import { ApiKey, IApplicationSummary } from "../api-key.model"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; + +export interface IMyApp { + selected: boolean; + name: string; + id: number; +} + +@Component({ + selector: 'apikey-create', + templateUrl: './create.component.html', + styleUrls: ['./create.component.scss'] +}) +export class CreateApiKeyComponent implements OnInit, OnDestroy { + apiKeyId: number = 0; + key: string = ''; + sharedSecret: string = ''; + applications: IMyApp[] = []; + title: string = ''; + accountId: number; + private appSub: any; + + constructor( + appService: ApplicationService, + authService: AuthorizeService, + private apiKeyService: ApiKeyService, + private toastrService: ToastrService, + private router: Router, + private route: ActivatedRoute, + private navMenuService: NavMenuService) { + + this.appSub = appService.applications.subscribe(x => this.updateApplications(x)); + this.key = Guid.newGuid(); + this.sharedSecret = Guid.newGuid(); + this.accountId = authService.user.accountId; + this.navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Api keys', route: ['admin/apikeys'] }, + { title: 'New', route: ['admin/apikeys/new'] } + ]); + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + this.appSub.unsubscribe(); + } + + async saveKey(): Promise { + var apiKey = new ApiKey(); + apiKey.title = this.title; + apiKey.applications = this.applications.filter(x => x.selected); + apiKey.accountId = this.accountId; + apiKey.apiKey = this.key; + apiKey.sharedSecret = this.sharedSecret; + + await this.apiKeyService.create(apiKey); + this.toastrService.success('Key is being created..'); + + this.router.navigate(['../../'], { relativeTo: this.route }); + } + + private updateApplications(apps: IApplication[]) { + var checkedOnes = this.applications.filter(x => x.selected).map(x => x.id); + var ourApps = apps.map(x => { + var map: IMyApp = { + id: x.id, + name: x.name, + selected: checkedOnes.indexOf(x.id) !== -1 + }; + return map; + }); + this.applications = ourApps; + } + + +} + +class Guid { + static newGuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.html new file mode 100644 index 00000000..a1388db4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.html @@ -0,0 +1,22 @@ +
+

{{key.title}}

+
+

Key

+ {{key.apiKey}} +

Shared secret

+ {{key.sharedSecret}} +

Allowed applications

+
+ {{item.name}} +
+
+ All applications are allowed when using this key. +
+ + +
+
+ Edit +
+
+ diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.spec.ts new file mode 100644 index 00000000..8a3acf47 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ApiKeyDetailsComponent } from './details.component'; + +describe('DetailsComponent', () => { + let component: ApiKeyDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ApiKeyDetailsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ApiKeyDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.ts new file mode 100644 index 00000000..c0831a6f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/details/details.component.ts @@ -0,0 +1,59 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ApiKeyService } from "../api-keys.service"; +import { ApiKey } from "../api-key.model"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; + +@Component({ + selector: 'app-details', + templateUrl: './details.component.html', + styleUrls: ['./details.component.scss'] +}) +export class ApiKeyDetailsComponent implements OnInit, OnDestroy { + apiKeyId: number = 0; + key: ApiKey = new ApiKey(); + + private appPromise: Promise; + private sub: any; + private id: number; + + constructor( + private route: ActivatedRoute, + private apiKeyService: ApiKeyService, + private navMenuService: NavMenuService) { + this.navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Api Keys', route: ['admin/apikeys'] } + ]); + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.id = +params['id']; + this.loadKey(); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + private async loadKey(): Promise { + await this.appPromise; + this.key = await this.apiKeyService.get(this.id); + this.navMenuService.updateNav([ + { title: 'System adminstration', route: ['admin'] }, + { title: 'Api keys', route: ['admin/apikeys'] }, + { title: this.key.title, route: ['admin/apikeys', this.key.id] } + ]); + } +} + +class Guid { + static newGuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.html new file mode 100644 index 00000000..bf0f250c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.html @@ -0,0 +1,37 @@ +
+
+

Edit API key

+
+
+
+
+ + + Name describing the usage of this API key. +
+
+
+ +
+
+

Accessible applications

+

Applications that they API key are allowed to control. Leave all check boxes unchecked to allow all applications.

+
+ + +
+
+ You can use the Coderr.Server.Api.Client nuget package to communicate using this key, or web sockets. +
+
+ +
+ + + +
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.spec.ts new file mode 100644 index 00000000..3a22d631 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditApiKeyComponent } from './edit.component'; + +describe('EditComponent', () => { + let component: EditApiKeyComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [EditApiKeyComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EditApiKeyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.ts new file mode 100644 index 00000000..a3782740 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/edit/edit.component.ts @@ -0,0 +1,129 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router, ActivatedRoute } from "@angular/router"; +import { IApplication } from "../../../applications/application.model"; +import { AuthorizeService } from "../../../../api-authorization/authorize.service"; +import { ApiKeyService } from "../api-keys.service"; +import { ApiKey, IApplicationSummary } from "../api-key.model"; +import { ToastrService } from "ngx-toastr"; +import { ApplicationService } from "../../../applications/application.service"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; + +export interface IMyApp { + selected: boolean; + name: string; + id: number; +} + +@Component({ + selector: 'app-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.scss'] +}) +export class EditApiKeyComponent implements OnInit, OnDestroy { + apiKeyId: number = 0; + key: string = ''; + sharedSecret: string = ''; + applications: IMyApp[] = []; + title: string = ''; + accountId: number; + + private appSub: any; + private sub: any; + private id: number; + + constructor( + appService: ApplicationService, + authService: AuthorizeService, + private route: ActivatedRoute, + private apiKeyService: ApiKeyService, + private toastrService: ToastrService, + private router: Router, + private navMenuService: NavMenuService) { + this.appSub = appService.applications.subscribe(x => this.updateApplications(x)); + this.key = Guid.newGuid(); + this.sharedSecret = Guid.newGuid(); + this.accountId = authService.user.accountId; + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.id = +params['id']; + this.loadKey(); + }); + } + + ngOnDestroy(): void { + this.appSub.unsubscribe(); + this.sub.unsubscribe(); + } + + back() { + this.router.navigate(['../'], { relativeTo: this.route }); + } + + async saveKey(): Promise { + var apiKey = new ApiKey(); + apiKey.id = this.id; + apiKey.title = this.title; + apiKey.applications = this.applications.filter(x=>x.selected); + apiKey.accountId = this.accountId; + apiKey.apiKey = this.key; + apiKey.sharedSecret = this.sharedSecret; + + await this.apiKeyService.update(apiKey); + this.toastrService.success('Key is being updated..'); + + this.router.navigate(['../'], { relativeTo: this.route }); + } + + private async loadKey(): Promise { + var key = await this.apiKeyService.get(this.id); + this.title = key.title; + this.accountId = key.accountId; + this.key = key.apiKey; + this.sharedSecret = key.sharedSecret; + + this.navMenuService.updateNav([ + { title: 'System adminstration', route: ['admin'] }, + { title: 'Api keys', route: ['admin/apikeys'] }, + { title: this.title, route: ['admin/apikeys', this.id] } + ]); + + if (this.applications.length === 0) { + key.applications.forEach(x => { + this.applications.push({ + id: x.id, + name: x.name, + selected: true + }); + }); + } else { + this.applications.forEach(x => { + x.selected = key.applications.find(y => y.id === x.id) != null; + }); + } + } + + private updateApplications(apps: IApplication[]) { + var checkedOnes = this.applications.filter(x => x.selected).map(x => x.id); + + var ourApps = apps.map(x => { + var map: IMyApp = { + id: x.id, + name: x.name, + selected: checkedOnes.indexOf(x.id) !== -1 + }; + return map; + }); + this.applications = ourApps; + } +} + +class Guid { + static newGuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { + var r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.html new file mode 100644 index 00000000..e157fa9b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.html @@ -0,0 +1,21 @@ +
+

Api keys

+

Api keys are used when communicating with Coderr through the HTTP or WebSocket API.

+ + + + + + + + + + + + + +
NameKey
{{key.title}}{{key.apiKey}}
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.spec.ts new file mode 100644 index 00000000..d271e9ee --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ApiKeyListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: ApiKeyListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ApiKeyListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ApiKeyListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.ts new file mode 100644 index 00000000..7788d89b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/apikeys/list/list.component.ts @@ -0,0 +1,27 @@ +import { Component, OnInit } from '@angular/core'; +import { ApiKeyService } from "../api-keys.service"; +import { IApiKeyListItem } from "../api-key.model"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; + +@Component({ + selector: 'app-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'] +}) +export class ApiKeyListComponent implements OnInit { + keys: IApiKeyListItem[] = []; + + constructor(private keyService: ApiKeyService, + private navMenuService: NavMenuService) { + keyService.list().then(x => this.keys = x); + this.navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Api Keys', route: ['admin/apikeys'] } + ]); + + } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.html new file mode 100644 index 00000000..71267171 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.html @@ -0,0 +1,23 @@ +
+ +
+ + +
+
+ + +
+
+ +
+
+ {{errorMessage}} + + +
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.spec.ts new file mode 100644 index 00000000..902df446 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CreateApplicationComponent } from './create.component'; + +describe('CreateComponent', () => { + let component: CreateApplicationComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [CreateApplicationComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CreateApplicationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.ts new file mode 100644 index 00000000..572fa4a9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/app-create/create.component.ts @@ -0,0 +1,69 @@ +import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { ApplicationService } from "../../applications/application.service"; +import { IGroupListItem } from "../groups/group.model"; +import { ApplicationGroupService } from "../groups/application-groups.service"; + +@Component({ + selector: 'app-create', + templateUrl: './create.component.html', + styleUrls: ['./create.component.scss'] +}) +export class CreateApplicationComponent implements OnInit, OnDestroy { + createForm = this.formBuilder.group({ + name: '', + groupId: 0, + trackStats: false + }); + errorMessage = ''; + returnUrl = ''; + groups: IGroupListItem[] = []; + showGroups = false; + disabled=false; + @Output() closed = new EventEmitter(); + + private sub: any; + + constructor( + private formBuilder: FormBuilder, + private router: Router, + groupService: ApplicationGroupService, + private applicationService: ApplicationService) { + this.sub = groupService.groups.subscribe(groups => { + this.groups = groups; + this.showGroups = this.groups.length > 0; + }); + + } + + ngOnInit() { + } + + ngOnDestroy() { + this.sub.unsubscribe(); + } + + cancel() { + this.closed.emit({ success: false, message: "Canceled" }); + } + + onSubmit(): void { + var groupId: number | null = null; + if (this.createForm.value.groupId !== "") { + groupId = parseInt(this.createForm.value.groupId, 10); + } + + this.disabled = true; + this.applicationService.create(this.createForm.value.name, groupId, null) + .then(x => { + this.createForm.reset(); + this.closed.emit({ success: true, application: x }); + this.router.navigate(['/']); + }).catch(e => { + this.closed.emit({ success: false, error: e }); + this.errorMessage = e.message.replace(/\n/g, "
\n"); + }); + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.html new file mode 100644 index 00000000..3985a633 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.html @@ -0,0 +1,14 @@ +
+ +
+ + +
+
+ {{errorMessage}} + + +
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.scss new file mode 100644 index 00000000..a5bd0972 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.scss @@ -0,0 +1,12 @@ +@import "../../../../styles/_partials/coderr-variables.scss"; + +.pills { + display: flex; + flex: 0 0 1fr; +} + +.pill { + background: $blue; + border-radius: 3px; + box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.spec.ts new file mode 100644 index 00000000..bc28a25e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupAddComponent } from './add.component'; + +describe('AddComponent', () => { + let component: GroupAddComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GroupAddComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupAddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.ts new file mode 100644 index 00000000..83d281c0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/add/add.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router, ActivatedRoute } from '@angular/router'; +import { ApplicationService } from "../../../applications/application.service"; +import { ApplicationGroupService } from "../application-groups.service"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; +import { IGroupListItem } from "../group.model"; +import { ToastrService } from "ngx-toastr"; + +export interface IGroupCreated { + success: boolean; + group?: IGroupListItem; + error?: Error; + + /** Can be "Canceled" if aborted (success = false) */ + message?: string; +} + +@Component({ + selector: 'app-group-add', + templateUrl: './add.component.html', + styleUrls: ['./add.component.scss'] +}) +export class GroupAddComponent implements OnInit, OnDestroy { + createForm = this.formBuilder.group({ + name: '', + }); + errorMessage = ''; + returnUrl = ''; + disabled = false; + @Output() closed = new EventEmitter(); + + constructor( + private formBuilder: FormBuilder, + private toastrService: ToastrService, + private router: Router, + private service: ApplicationGroupService, + navMenuService: NavMenuService) { + + // can be included in other pages. + if (this.router.url.includes('/groups/new')) { + navMenuService.updateNav([ + { title: 'System Administration', route: ['/admin'] }, + { title: 'Application Groups', route: ['/admin/groups'] }, + { title: 'New', route: ['/admin/groups/new'] } + ]); + + } + } + + ngOnInit() { + } + + ngOnDestroy() { + } + + cancel() { + this.closed.emit({ success: false, message: "Canceled" }); + } + + onSubmit(): void { + this.disabled = true; + this.service.create(this.createForm.value.name) + .then(x => { + this.toastrService.success('Entry is being created..'); + this.createForm.reset(); + this.closed.emit({ success: true, group: x }); + }).catch(e => { + this.closed.emit({ success: false, error: e }); + this.errorMessage = e.message.replace(/\n/g, "
\n"); + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/application-groups.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/application-groups.service.spec.ts new file mode 100644 index 00000000..63f1cb26 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/application-groups.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ApplicationGroupService as ApplicationGroupsService } from './application-groups.service'; + +describe('ApplicationGroupService', () => { + let service: ApplicationGroupsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ApplicationGroupsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/application-groups.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/application-groups.service.ts new file mode 100644 index 00000000..fb91cb91 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/application-groups.service.ts @@ -0,0 +1,154 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubjectList } from "../../utils/SubjectList"; +import * as model from "./group.model"; +import { ApiClient } from "../../utils/HttpClient"; +import { SignalRService } from "../../services/signal-r.service"; +import { AuthorizeService, IUser } from "../../../api-authorization/authorize.service"; +import * as api from "../../../server-api/Core/Applications"; +import IGroup = model.IGroup; +import Group = model.Group; +import { PromiseWrapper } from "../../PromiseWrapper"; + +@Injectable({ + providedIn: 'root' +}) +export class ApplicationGroupService implements OnDestroy { + private grps = new BehaviorSubjectList((a, b) => a.name.localeCompare(b.name)); + private allGroups: Group[] = []; + private loadPromise: PromiseWrapper = new PromiseWrapper(); + private userSub: any; + + constructor( + private client: ApiClient, + private authService: AuthorizeService, + private signalHub: SignalRService + ) { + this.userSub = this.authService.userEvents.subscribe(x => this.onAuth(x)); + } + + ngOnDestroy(): void { + this.userSub.unsubscribe(); + } + + get groups(): BehaviorSubject { + return this.grps.subject; + } + + async get(id: number): Promise { + if (!id) { + throw new Error("Id must be specified!"); + } + + await this.loadPromise.promise; + + if (this.allGroups.length === 0) { + await this.loadPromise.promise; + } + + var group = this.allGroups.find(x => x.id === id); + if (!group) { + throw new Error("Failed to find group " + id); + } + + return group; + } + + async remove(groupId: number): Promise { + var cmd = new api.DeleteApplicationGroup(); + cmd.groupId = groupId; + await this.client.command(cmd); + } + + async list(): Promise { + await this.loadPromise.promise; + return this.allGroups; + } + + async getGroupsForApplication(applicationId: number): Promise { + await this.loadPromise.promise; + + return this.allGroups.filter(x => x.applications.includes(applicationId)).map(x => x.id); + } + + async create(name: string): Promise { + if (!name) { + throw new Error("Name must be specified!"); + } + + var waitPromise = this.signalHub.wait(x => + x.typeName === "ApplicationGroupCreated" && + x.body.createdById === this.authService.user.accountId); + + var cmd = new api.CreateApplicationGroup(); + cmd.name = name; + await this.client.command(cmd); + + var event = await waitPromise; + + var group: model.IGroupListItem = { + id: event.body.id, + name: name + }; + this.grps.add(group); + return group; + } + + async update(group: IGroup): Promise { + if (!group) { + throw new Error("Group must be specified!"); + } + + var cmd = new api.RenameApplicationGroup(); + cmd.newName = group.name; + cmd.groupId = group.id; + await this.client.command(cmd); + + var cmd2 = new api.MapApplicationsToGroup(); + cmd2.groupId = group.id; + cmd2.applicationIds = group.applications; + await this.client.command(cmd2); + + var entity = this.allGroups.find(x => x.id === group.id); + entity.name = group.name; + entity.applications = group.applications; + } + + private async loadGroups2(): Promise { + const query = new api.GetApplicationGroups(); + const result = await this.client.query(query); + if (!result) { + return; + } + + var allGroups: model.IGroupListItem[] = []; + result.items.forEach(group => { + allGroups.push({ + id: group.id, + name: group.name + }); + }); + + this.allGroups = allGroups.map(x => { return { id: x.id, name: x.name, applications: [], teams: [] } }); + + const query2 = new api.GetApplicationGroupMap(); + const result2 = await this.client.query(query2); + if (!result2) { + return; + } + + result2.items.forEach(map => { + var group = this.allGroups.find(y => map.groupId === y.id); + group.applications.push(map.applicationId); + }); + + this.grps.addAll(allGroups); + this.loadPromise.accept(null); + } + + private onAuth(user: IUser): void { + if (user) { + this.loadGroups2(); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.html new file mode 100644 index 00000000..6b12e635 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.html @@ -0,0 +1,34 @@ +
+

{{group.name}}

+
+

Applications

+
    +
  • + {{app.name}} +
  • +
+
+

No applications have been added to the group.

+
+ +
+
+ Edit + Remove +
+
+ + +
+

Confirm removal

+
+
+
+ Are you sure that you want to remove this group? All applications will be moved to the generic group. +
+ + +
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.spec.ts new file mode 100644 index 00000000..99cbf52a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupDetailsComponent } from './details.component'; + +describe('DetailsComponent', () => { + let component: GroupDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GroupDetailsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.ts new file mode 100644 index 00000000..b79ad392 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/details/details.component.ts @@ -0,0 +1,72 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ApplicationGroupService } from "../application-groups.service"; +import { IGroup, Group } from "../group.model"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; +import { ApplicationService } from "../../../applications/application.service"; +import { IApplicationListItem } from "../../whitelist/whitelist.model"; +import { ModalService } from "../../../_controls/modal/modal.service"; + +@Component({ + selector: 'group-details', + templateUrl: './details.component.html', + styleUrls: ['./details.component.scss'] +}) +export class GroupDetailsComponent implements OnInit, OnDestroy { + group: IGroup = new Group(0, ''); + apps: IApplicationListItem[] = []; + id: number; + private routeSub: any; + + constructor( + private groupService: ApplicationGroupService, + private appService: ApplicationService, + private modalService: ModalService, + private route: ActivatedRoute, + private router: Router, + private navMenuService: NavMenuService) { + } + + ngOnInit(): void { + this.routeSub = this.route.params.subscribe(params => { + this.id = +params['id']; + this.load(this.id); + }); + } + + ngOnDestroy(): void { + this.routeSub.unsubscribe(); + } + + verifyRemove() { + this.modalService.open('verifyRemoveModal'); + } + + async removeEntry(): Promise { + await this.groupService.remove(this.id); + this.modalService.close('verifyRemoveModal'); + this.router.navigate(['../'], { relativeTo: this.route }); + } + + cancelRemove() { + this.modalService.close('verifyRemoveModal'); + } + + private async load(id: number): Promise { + this.group = await this.groupService.get(id); + var apps = await this.appService.list(); + this.apps = this.group.applications.map(x => { + return { + id: x, + name: apps.find(y => y.id === x).name + } + }); + + this.navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Application Groups', route: ['admin/groups'] }, + { title: this.group.name, route: ['admin/groups/', id] } + ]); + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.html new file mode 100644 index 00000000..99394c67 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.html @@ -0,0 +1,31 @@ +
+
+

Edit group '{{name}}'

+
+
+ + +
+
+ +

+ Application(s) that are displayed under this group. +

+
+ + +
+

+ Only unassigned applications are shown here, to move an application, edit its settings. +

+
+
+
+ + +
+
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.spec.ts new file mode 100644 index 00000000..7b37b4d7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupEditComponent } from './edit.component'; + +describe('EditComponent', () => { + let component: GroupEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GroupEditComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.ts new file mode 100644 index 00000000..c2c57657 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/edit/edit.component.ts @@ -0,0 +1,106 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ApplicationService } from "../../../applications/application.service"; +import { ActivatedRoute, Router } from "@angular/router"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; +import { ApplicationGroupService } from "../application-groups.service"; +import { IGroup, Group } from "../group.model"; +import { IApplication } from "../../../applications/application.model"; + +export interface IApplicationSelection { + selected: boolean; + name: string; + id: number; +} + +@Component({ + selector: 'app-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.scss'] +}) +export class GroupEditComponent implements OnInit, OnDestroy { + applications: IApplicationSelection[] = []; + group: IGroup; + name = ''; + disabled = false; + private allApps: IApplication[] = []; + private dbGroupApps: number[] = []; + private id: number; + private routeSub: any; + private appSub: any; + + constructor(appService: ApplicationService, + private service: ApplicationGroupService, + private route: ActivatedRoute, + private router: Router, + private navMenuService: NavMenuService) { + this.appSub = appService.applications.subscribe(x => { + this.allApps = x; + this.updateApplications(); + }); + } + + ngOnInit(): void { + this.routeSub = this.route.params.subscribe(params => { + this.id = +params['id']; + this.load(this.id); + }); + } + + ngOnDestroy(): void { + this.routeSub.unsubscribe(); + this.appSub.unsubscribe(); + } + + save() { + this.disabled = true; + var group = new Group(this.group.id, this.name); + group.applications = this.applications.filter(x => x.selected).map(x => x.id); + this.service.update(group); + this.router.navigate(['..'], { relativeTo: this.route }); + } + + cancel() { + this.disabled = false; + this.router.navigate(['../'], { relativeTo: this.route }); + } + + private async load(id: number): Promise { + this.group = await this.service.get(id); + this.name = this.group.name; + this.dbGroupApps = this.group.applications; + if (this.dbGroupApps.length > 0) { + this.updateApplications(); + } + + this.navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Application Groups', route: ['admin/groups'] }, + { title: this.name, route: ['admin/groups/', id] } + ]); + + } + + private updateApplications() { + if (this.allApps.length === 0) { + return; + } + + var selectedApps = this.applications.filter(x => x.selected).map(x => x.id); + if (selectedApps.length === 0) { + selectedApps = this.dbGroupApps; + this.dbGroupApps = []; + } + + var ourApps = this.allApps.map(x => { + var map: IApplicationSelection = { + id: x.id, + name: x.name, + selected: selectedApps.includes(x.id) || x.groupIds.includes(this.id) + }; + + return map; + }); + + this.applications = ourApps; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.model.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.model.spec.ts new file mode 100644 index 00000000..cbdb0760 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.model.spec.ts @@ -0,0 +1,7 @@ +import { Group } from './group.model'; + +describe('Group', () => { + it('should create an instance', () => { + expect(new Group(1, 'lala')).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.model.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.model.ts new file mode 100644 index 00000000..d2ab096b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.model.ts @@ -0,0 +1,25 @@ +export class Group implements IGroup { + constructor(public id: number, public name: string) { + } + + teams: IGroupTeam[] = []; + applications: number[] = []; +} + +export interface IGroup { + readonly id: number; + readonly name: string; + readonly teams: IGroupTeam[]; + readonly applications: number[]; +} + +export interface IGroupListItem { + readonly id: number; + readonly name: string; +} + + +export interface IGroupTeam { + readonly teamId: number; + readonly teamName: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.module.ts new file mode 100644 index 00000000..bafd4ccb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/group.module.ts @@ -0,0 +1,34 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { ControlsModule } from "../../_controls/controls.module"; + +import { GroupAddComponent } from './add/add.component'; +import { GroupEditComponent } from './edit/edit.component'; +import { GroupDetailsComponent } from './details/details.component'; +import { GroupListComponent } from './list/list.component'; + +export const groupRoutes: Routes = [ + { path: 'groups/new', component: GroupAddComponent }, + { path: 'groups', component: GroupListComponent }, + { path: 'groups/:id', component: GroupDetailsComponent }, + { path: 'groups/:id/edit', component: GroupEditComponent } +]; + + +@NgModule({ + declarations: [GroupAddComponent, GroupListComponent, GroupDetailsComponent, GroupEditComponent], + imports: [ + ControlsModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + RouterModule + ], + exports: [ + GroupAddComponent, + GroupListComponent + ] +}) +export class GroupModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.html new file mode 100644 index 00000000..8f6e08b7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.html @@ -0,0 +1,35 @@ +
+

Application groups

+

Groups let's you divide your applications into smaller groups.

+ + + + + + + + + + + + +

No groups have been specified.

+ + +
Group name
+ {{group.name}} +
+
+ Create +
+
+ + +
+

Create group

+
+

Application groups are used to logically group applications into smaller lists.

+ +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.spec.ts new file mode 100644 index 00000000..4b7f1aa9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupListComponent as GroupsListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: GroupsListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GroupsListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupsListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.ts new file mode 100644 index 00000000..f57f34e2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/groups/list/list.component.ts @@ -0,0 +1,51 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { IGroupListItem } from "../group.model"; +import { ApplicationGroupService } from "../application-groups.service"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; +import { ModalService } from "../../../_controls/modal/modal.service"; +import { IGroupCreated } from "../add/add.component"; + +@Component({ + selector: 'app-groups', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'] +}) +export class GroupListComponent implements OnInit, OnDestroy { + groups: IGroupListItem[]; + private sub: any; + + constructor( + private groupService: ApplicationGroupService, + private modalService: ModalService, + navMenuService: NavMenuService) { + + navMenuService.updateNav([ + { title: 'System Administration', route: ['/admin'] }, + { title: 'Application Groups', route: ['/admin/groups'] } + ]); + } + + ngOnInit(): void { + this.sub = this.groupService.groups.subscribe(groups => { + this.groups = groups; + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + createGroup() { + this.modalService.open('createNewGroupModal'); + } + + onGroupCreated(e: IGroupCreated) { + if (e.success) { + this.groups.push(e.group); + } + + this.modalService.close('createNewGroupModal'); + } + + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.html new file mode 100644 index 00000000..0994686b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.html @@ -0,0 +1,12 @@ + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.scss new file mode 100644 index 00000000..15a44a79 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.scss @@ -0,0 +1,34 @@ +@import "../../../styles/_partials/coderr-variables.scss"; +@import "../../../styles/_partials/_mixins.scss"; + +.submenu { + color: #ddd; + background: $nav-sub-bg; + padding-left: 15px; + padding-right: 15px; + + + .actions { + margin-left: auto; + margin-bottom: 3px; + margin-top: auto; + + a { + color: #444; + padding: 3px 5px; + /* border-top: 1px solid darken($blue, 20%); + border-left: 2px solid darken($blue, 15%); + /* border-right: 1px solid darken($blue, 20%);*/ + border-left: 2px solid $nav-sub-bg; + background-color: $blue; + + &.active { + color: #fff; + } + + &:hover { + color: $red; + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.spec.ts new file mode 100644 index 00000000..2220db5e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminNavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: AdminNavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AdminNavbarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminNavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.ts new file mode 100644 index 00000000..7274f408 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/navbar/navbar.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { ToastrService } from "ngx-toastr"; +import { ModalService } from "../../_controls/modal/modal.service"; +import { AccountService } from "../../accounts/account.service"; + +@Component({ + selector: 'admin-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'] +}) +export class AdminNavbarComponent implements OnInit { + + constructor( + private modalService: ModalService, + private toastrService: ToastrService, + private accountService: AccountService + ) { + } + + ngOnInit(): void { + } + + + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.html new file mode 100644 index 00000000..cc419d46 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.html @@ -0,0 +1,68 @@ + +
+
+

New whitelist

+ +
+
+ + + Domain name, including the subdomain/host. +
+
+ +

+ Application(s) that the domain may report errors for. None marked = all are allowed. +

+
+ + +
+
+
+ +

+ + Per default, Coderr will lookup all IP addresses (through DNS) that are associated with a domain name. + Here you can manually specify which IP addresses that are allowed for the domain, and by doing so disable the DNS lookup. + +

+ + + + +
+ +
+ +
+
+ {{errorMessage}} +
+
+ + + +
+
+ + +
+

New IP address

+
+
+ + +
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.spec.ts new file mode 100644 index 00000000..9d7e2fa8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddComponent } from './add.component'; + +describe('AddComponent', () => { + let component: AddComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AddComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.ts new file mode 100644 index 00000000..d7580d70 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/add/add.component.ts @@ -0,0 +1,104 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router, ActivatedRoute } from "@angular/router"; +import { IApplication } from '../../../applications/application.model'; +import { ApplicationService } from '../../../applications/application.service'; +import { NavMenuService } from '../../../nav-menu/nav-menu.service'; +import { ModalService } from '../../../_controls/modal/modal.service'; +import { IApplicationListItem, IIpAddress, IpType, WhitelistEntry } from '../whitelist.model'; +import { WhitelistService } from '../whitelist.service'; + +export interface IMyApp { + selected: boolean; + name: string; + id: number; +} + +@Component({ + selector: 'whitelist-add', + templateUrl: './add.component.html', + styleUrls: ['./add.component.scss'] +}) +export class AddComponent implements OnInit, OnDestroy { + domainName = ""; + applications: IMyApp[] = []; + ipAddresses: IIpAddress[] = []; + newIpAddress = ''; + errorMessage = ''; + disabled = false; + private appSub: any; + + constructor(appService: ApplicationService, + private modalService: ModalService, + private navMenuService: NavMenuService, + private route: ActivatedRoute, + private router: Router, + private service: WhitelistService) { + this.appSub = appService.applications.subscribe(x => this.updateApplications(x)); + this.navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Whitelists', route: ['admin/whitelists'] }, + { title: 'New', route: ['admin/whitelists/new'] } + ]); + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + this.appSub.unsubscribe(); + } + + showAddIp() { + this.modalService.open('newIpModal'); + } + + closeIpModal() { + this.modalService.close('newIpModal'); + } + + addNewIp() { + this.ipAddresses.push({ + address: this.newIpAddress, + id: 0, + type: IpType.Manual + }); + this.newIpAddress = ''; + this.closeIpModal(); + } + + cancel() { + this.router.navigate(['../'], { relativeTo: this.route }); + } + + async save(): Promise { + this.disabled = true; + var entry = new WhitelistEntry(this.domainName); + entry.applications = this.applications.filter(x => x.selected).map(x => { + var e: IApplicationListItem = { + id: x.id, + name: x.name + }; + return e; + }); + entry.ipAddresses = this.ipAddresses; + await this.service.add(entry); + + this.router.navigate(['../'], { relativeTo: this.route }); + } + + private updateApplications(apps: IApplication[]) { + + var checkedOnes = this.applications.filter(x => x.selected).map(x => x.id); + + var ourApps = apps.map(x => { + var map: IMyApp = { + id: x.id, + name: x.name, + selected: checkedOnes.indexOf(x.id) !== -1 + }; + return map; + }); + + this.applications = ourApps; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.html new file mode 100644 index 00000000..5b7e7634 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.html @@ -0,0 +1,34 @@ +
+

{{whitelist.domainName}}

+
+

Allowed applications

+
    +
  • {{app.name}}
  • +
+ All +

Allowed IP addresses

+
    +
  • {{ip.address}}
  • +
+
+
 
+
+ Edit + Remove +
+
+ + +
+

Confirm removal

+
+
+
+ Are you sure that you want to remove this whitelist? +
+ + +
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.spec.ts new file mode 100644 index 00000000..3505e504 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DetailsComponent } from './details.component'; + +describe('DetailsComponent', () => { + let component: DetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DetailsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.ts new file mode 100644 index 00000000..5f01db40 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/details/details.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router, ActivatedRoute } from "@angular/router"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; +import { WhitelistService } from "../whitelist.service"; +import { WhitelistEntry } from "../whitelist.model"; +import { ModalService } from "../../../_controls/modal/modal.service"; + +@Component({ + selector: 'whitelist-details', + templateUrl: './details.component.html', + styleUrls: ['./details.component.scss'] +}) +export class DetailsComponent implements OnInit, OnDestroy { + whitelist: WhitelistEntry = new WhitelistEntry('temp.com'); + id: number; + private routeSub: any; + + constructor( + private service: WhitelistService, + private modalService: ModalService, + private route: ActivatedRoute, + private router: Router, + private navMenuService: NavMenuService, + ) { + + + } + + ngOnInit(): void { + this.routeSub = this.route.params.subscribe(params => { + this.id = +params['id']; + this.load(this.id); + }); + } + + ngOnDestroy(): void { + this.routeSub.unsubscribe(); + } + + verifyRemove() { + this.modalService.open('verifyRemoveModal'); + } + + async removeEntry(): Promise { + await this.service.remove(this.id); + this.modalService.close('verifyRemoveModal'); + this.router.navigate(['../'], { relativeTo: this.route }); + } + + cancelRemove() { + this.modalService.close('verifyRemoveModal'); + } + + + private async load(id: number): Promise { + this.whitelist = await this.service.get(id); + + this.navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Whitelists', route: ['admin/whitelists'] }, + { title: this.whitelist.domainName, route: ['admin/whitelists/', id] } + ]); + + } + + + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.html new file mode 100644 index 00000000..261a8015 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.html @@ -0,0 +1,72 @@ +
+
+

Edit whitelist for '{{domainName}}'

+
+
+ + + Domain name, including the subdomain/host. +
+
+ +

+ Application(s) that the domain may report errors for. None marked = all are allowed. +

+
+ + +
+
+
+ +

+ + Per default, Coderr will lookup all IP addresses (through DNS) that are associated with a domain name. + Here you can manually specify which IP addresses that are allowed for the domain, and by doing so disable the DNS lookup. + +

+ + + + +
+
+ + +
+
+ +
+
+ {{errorMessage}} +
+
+
+ + +
+
+ +
+ + +
+

New IP address

+
+
+ + +
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.spec.ts new file mode 100644 index 00000000..5a24d45a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditComponent } from './edit.component'; + +describe('EditComponent', () => { + let component: EditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EditComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.ts new file mode 100644 index 00000000..d5783891 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/edit/edit.component.ts @@ -0,0 +1,148 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Router, ActivatedRoute } from "@angular/router"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; +import { WhitelistService } from "../whitelist.service"; +import { WhitelistEntry, IIpAddress, IApplicationListItem, IpType } from "../whitelist.model"; +import { ApplicationService } from "../../../applications/application.service"; +import { ModalService } from "../../../_controls/modal/modal.service"; + +export interface IApplicationSelection { + selected: boolean; + name: string; + id: number; +} + +@Component({ + selector: 'whitelist-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.scss'] +}) +export class EditComponent implements OnInit, OnDestroy { + id: number; + domainName = ""; + applications: IApplicationSelection[] = []; + ipAddresses: IIpAddress[] = []; + newIpAddress = ''; + errorMessage = ''; + disabled = false; + private routeSub: any; + private appSub: any; + + constructor( + appService: ApplicationService, + private modalService: ModalService, + private service: WhitelistService, + private route: ActivatedRoute, + private router: Router, + private navMenuService: NavMenuService) { + this.appSub = appService.applications.subscribe(x => { + var apps = x.map(x => { + var app: IApplicationSelection = { + id: x.id, + name: x.name, + selected: false + }; + return app; + }); + this.updateApplications(apps); + }); + + + } + + ngOnInit(): void { + this.routeSub = this.route.params.subscribe(params => { + this.id = +params['id']; + this.load(this.id); + }); + } + + ngOnDestroy(): void { + this.routeSub.unsubscribe(); + this.appSub.unsubscribe(); + } + + showAddIp() { + this.modalService.open('newIpModal'); + } + + closeIpModal() { + this.modalService.close('newIpModal'); + } + + addNewIp() { + this.ipAddresses.push({ + address: this.newIpAddress, + id: 0, + type: IpType.Manual + }); + this.newIpAddress = ''; + this.closeIpModal(); + } + + cancel() { + + } + + removeIp(entry: IIpAddress) { + this.ipAddresses = this.ipAddresses.filter(x => x.address !== entry.address); + } + + private async load(id: number): Promise { + var whitelist = await this.service.get(id); + this.domainName = whitelist.domainName; + + var apps = this.applications; + this.applications = whitelist.applications.map(x => { + var entry: IApplicationSelection = { + id: x.id, + name: x.name, + selected: true + }; + return entry; + }); + if (apps.length > 0) { + this.updateApplications(apps); + } + + this.ipAddresses = whitelist.ipAddresses; + + this.navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Whitelists', route: ['admin/whitelists'] }, + { title: this.domainName, route: ['admin/whitelists/', id] } + ]); + + } + + async save(): Promise { + this.disabled = true; + var entry = new WhitelistEntry(this.domainName); + entry.applications = this.applications.filter(x => x.selected).map(x => { + var e: IApplicationListItem = { + id: x.id, + name: x.name + }; + return e; + }); + entry.ipAddresses = this.ipAddresses; + await this.service.update(entry); + + this.router.navigate(['../'], { relativeTo: this.route }); + } + + private updateApplications(apps: IApplicationSelection[]) { + var checkedOnes = this.applications.filter(x => x.selected).map(x => x.id); + var ourApps = apps.map(x => { + var map: IApplicationSelection = { + id: x.id, + name: x.name, + selected: checkedOnes.indexOf(x.id) !== -1 + }; + return map; + }); + + this.applications = ourApps; + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.html new file mode 100644 index 00000000..f79f2b92 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.html @@ -0,0 +1,24 @@ +
+

Whitelists

+

A secret key cannot be used when reporting errors from websites. To avoid that anyone can report errors to your account, you can whitelist your domains and servers.

+ + + + + + + + + + + + + + + +
Domain nameAllowed applicationsWhite-listed ip addresses
{{entry.domainName}}
+
 
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.scss new file mode 100644 index 00000000..b8474b1e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.scss @@ -0,0 +1,3 @@ +table td, table th { + border: 1px; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.spec.ts new file mode 100644 index 00000000..a5d3a5c3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: ListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.ts new file mode 100644 index 00000000..3ef011f5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/list/list.component.ts @@ -0,0 +1,48 @@ +import { Component, OnInit } from '@angular/core'; +import { IApplication } from '../../../applications/application.model'; +import { ApplicationService } from '../../../applications/application.service'; +import { IApplicationListItem, IIpAddress, WhitelistEntry } from '../whitelist.model'; +import { WhitelistService } from "../whitelist.service"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; + +@Component({ + selector: 'whitelist', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'] +}) +export class ListComponent implements OnInit { + entries: WhitelistEntry[] = []; + private apps: IApplication[] = []; + + constructor( + private appService: ApplicationService, + navMenuService: NavMenuService, + private service: WhitelistService) { + + navMenuService.updateNav([ + { title: 'System Administration', route: ['admin'] }, + { title: 'Whitelists', route: ['admin/whitelists'] } + ]); + + this.load(); + } + + ngOnInit(): void { + } + + friendlyIps(items: IIpAddress[]) { + return items.length === 0 ? 'All' : items.map(x => x.address).join('
'); + } + + friendlyApps(items: IApplicationListItem[]) { + return items.length === 0 ? 'All' : items.map(x=>x.name).join('
'); + } + + private async load(): Promise { + this.entries = await this.service.list(); + this.apps = await this.appService.list(); + this.entries.forEach(x => { + x.mapAllApps(this.apps); + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.model.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.model.spec.ts new file mode 100644 index 00000000..320a6ae8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.model.spec.ts @@ -0,0 +1,7 @@ +import { WhitelistEntry } from './whitelist.model'; + +describe('Whitelist', () => { + it('should create an instance', () => { + expect(new WhitelistEntry('arnwe', 1)).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.model.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.model.ts new file mode 100644 index 00000000..243cafed --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.model.ts @@ -0,0 +1,51 @@ +import { IApplication } from "../../applications/application.model"; + +export interface IWhitelistApp extends IApplicationListItem { + selected: boolean; +} + +export interface IApplicationListItem { + id: number; + name: string; +} + + +export interface IIpAddress { + id: number; + address: string; + type: IpType; +} + +export enum IpType { + Lookup = 0, + Manual = 1, + Rejected = 2 +} + +export class WhitelistEntry { + constructor(public domainName: string, public id?: number) { + + } + + applications: IApplicationListItem[] = []; + ipAddresses: IIpAddress[] = []; + + /** + * Create a list with all apps and make ours selected. + * @param apps All applications that the user has permissions to. + */ + mapAllApps(apps: IApplication[]): IWhitelistApp[] { + if (!apps) { + throw new Error("apps are required."); + } + + var selectedIds = this.applications.map(x => x.id); + return apps.map(x => { + return { + id: x.id, + name: x.name, + selected: selectedIds.indexOf(x.id) >= 0 + }; + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.module.ts new file mode 100644 index 00000000..54f90701 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.module.ts @@ -0,0 +1,29 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { ControlsModule } from "../../_controls/controls.module"; + +import { AddComponent } from './add/add.component'; +import { EditComponent } from './edit/edit.component'; +import { DetailsComponent } from './details/details.component'; +import { ListComponent } from './list/list.component'; + +export const whitelistRoutes: Routes = [ + { path: 'whitelists/new', component: AddComponent }, + { path: 'whitelists', component: ListComponent }, + { path: 'whitelists/:id', component: DetailsComponent }, + { path: 'whitelists/:id/edit', component: EditComponent } +]; + + +@NgModule({ + declarations: [AddComponent, EditComponent, DetailsComponent, ListComponent], + imports: [ + CommonModule, + FormsModule, + ControlsModule, + RouterModule + ] +}) +export class WhitelistModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.service.spec.ts new file mode 100644 index 00000000..f02e093c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { WhitelistService } from './whitelist.service'; + +describe('WhitelistService', () => { + let service: WhitelistService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(WhitelistService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.service.ts new file mode 100644 index 00000000..bb7c8dee --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/admin/whitelist/whitelist.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@angular/core'; +import { ApiClient } from "../../utils/HttpClient"; +import * as whitelist from "../../../server-api/Core/Whitelist"; +import { WhitelistEntry, IWhitelistApp, IIpAddress, IpType } from "./whitelist.model"; +import { ApplicationService } from "../../applications/application.service"; + +@Injectable({ + providedIn: 'root' +}) +export class WhitelistService { + + constructor( + private apiClient: ApiClient, + private applicationService: ApplicationService) { } + + async list(): Promise { + var entities: WhitelistEntry[] = []; + var q = new whitelist.GetWhitelistEntries(); + var result = await this.apiClient.query(q); + result.entries.forEach(dto => { + + const entity = this.convertEntry(dto); + entities.push(entity); + }); + + return entities; + } + + async get(id: number): Promise { + if (!id) { + throw new Error("Must specify an id."); + } + + var q = new whitelist.GetWhitelistEntries(); + var result = await this.apiClient.query(q); + + const item = result.entries.find(x => x.id === id); + if (!item) { + throw new Error("Failed to find item " + id); + } + + return this.convertEntry(item); + } + + async add(entry: WhitelistEntry): Promise { + if (!entry) { + throw new Error("Must specify an entry to add."); + } + + var cmd = new whitelist.AddEntry(); + cmd.applicationIds = entry.applications.map(x=>x.id); + cmd.domainName = entry.domainName; + cmd.ipAddresses = entry.ipAddresses.map(x => x.address); + this.apiClient.command(cmd); + } + + async update(entry: WhitelistEntry): Promise { + if (!entry) { + throw new Error("Must specify an entry to update it."); + } + + if (!entry.id) { + throw new Error("Must specify an entry ID to update it."); + } + + var cmd = new whitelist.EditEntry(); + cmd.id = entry.id; + cmd.applicationIds = entry.applications.map(x => x.id); + cmd.domainName = entry.domainName; + cmd.ipAddresses = entry.ipAddresses.map(x => x.address); + this.apiClient.command(cmd); + } + + async remove(id: number) { + if (!id) { + throw new Error("Must specify an ID to remove the entry."); + } + + var cmd = new whitelist.RemoveEntry(); + cmd.id = id; + this.apiClient.command(cmd); + } + + private convertEntry(dto: whitelist.GetWhitelistEntriesResultItem): WhitelistEntry { + if (!dto) { + throw new Error("Must specify a DTO for conversion"); + } + + var entity = new WhitelistEntry(dto.domainName, dto.id); + entity.applications = dto.applications.map(x => { + return { + id: x.applicationId, + name: x.name, + selected: true + }; + }); + + entity.ipAddresses = dto.ipAddresses.map(x => { + return { + id: x.id, + address: x.address, + type: x.type + }; + }); + + return entity; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.component.html new file mode 100644 index 00000000..ff74881d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.component.html @@ -0,0 +1,4 @@ + +
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.component.ts new file mode 100644 index 00000000..24a46b45 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.component.ts @@ -0,0 +1,9 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html' +}) +export class AppComponent { + title = 'Coderr'; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.module.ts new file mode 100644 index 00000000..78946a00 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.module.ts @@ -0,0 +1,62 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './app.component'; +import { NavMenuComponent } from './nav-menu/nav-menu.component'; +import { ApiAuthorizationModule } from 'src/api-authorization/api-authorization.module'; +import { AuthorizeGuard } from 'src/api-authorization/authorize.guard'; +import { AuthorizeInterceptor } from 'src/api-authorization/authorize.interceptor'; + +import { HomeModule } from "./home/home.module"; +//import { AppDashboardComponent } from "./applications/user/dashboard.component"; +import { IncidentsModule } from "./incidents/incidents.module"; +import { AccountModule } from "./accounts/account.module"; +import { AdminModule } from "./admin/admin.module"; + +import { ControlsModule } from "./_controls/controls.module"; +import { SignalRService } from "./services/signal-r.service"; +import { ToastrModule } from 'ngx-toastr'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; + +var routes = [ + //{ path: '', component: HomeComponent, pathMatch: 'full', canActivate: [AuthorizeGuard] }, + //{ path: 'dashboard', component: AppDashboardComponent, canActivate: [AuthorizeGuard] } +]; + +@NgModule({ + declarations: [ + AppComponent, + NavMenuComponent + ], + imports: [ + BrowserModule.withServerTransition({ appId: 'ng-cli-universal' }), + ControlsModule, + HttpClientModule, + CommonModule, + FormsModule, + ReactiveFormsModule, + ApiAuthorizationModule, + RouterModule.forRoot(routes), + AdminModule, + IncidentsModule, + HomeModule, + AccountModule, + ToastrModule.forRoot(), + BrowserAnimationsModule + ], + exports: [ + ], + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: AuthorizeInterceptor, multi: true }, + ], + bootstrap: [AppComponent] +}) +export class AppModule { + constructor(service: SignalRService) { + service.startConnection(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.server.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.server.module.ts new file mode 100644 index 00000000..316380b6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/app.server.module.ts @@ -0,0 +1,10 @@ +import { NgModule } from '@angular/core'; +import { ServerModule } from '@angular/platform-server'; +import { AppComponent } from './app.component'; +import { AppModule } from './app.module'; + +@NgModule({ + imports: [AppModule, ServerModule], + bootstrap: [AppComponent] +}) +export class AppServerModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.html new file mode 100644 index 00000000..885917a4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.html @@ -0,0 +1,76 @@ +
+
+

Configure application

+
+

+ To configure error reporting in your application, click on one of the buttons below. +

+
+ {{name}} +
+
+

Select library

+

Now, select the libraries that you want to use to report errors (with automatically collected telemetry data).

+
+
+ {{tagStr(library)}}
+ {{library.id}}
+ {{library.description}} +
+ +
+
+ +
+

Instruction

+
+
+ + +
+
+
+

Get started faster

+
+

+ Want help getting everything going? Leave us a message below and we'll answer if we are awake (we're located in Sweden). +

+
+
+
+
+
+ +
+ +
+
+
+ +
+ +

+ To configure error reporting in your application, click on one of the buttons below. +

+
+ {{name}} +
+
+

Select library

+

Now, select the libraries that you want to use to report errors (with automatically collected telemetry data).

+
+
+ {{tagStr(library)}}
+ {{library.id}}
+ {{library.description}} +
+ +
+
+ +
+

Instruction

+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.scss new file mode 100644 index 00000000..18691b62 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.scss @@ -0,0 +1,41 @@ +@import "../../../../styles/_partials/coderr-variables.scss"; + +.library { + flex: 1; + padding: 10px; + margin-right: 5px; + margin-bottom: 5px; + background-color: $blue; + color: #fff; + display: inline-block; + min-width: 300px; + cursor: pointer; + flex-direction: column; + + strong { + font-size: 1.2em; + } + + .category { + text-align: right; + color: $light; + font-size: 0.9em; + font-style: italic; + padding-bottom: 5px; + display: block; + } + + .description { + display: block; + color: #fafafa; + font-size: 0.95em; + font-style: italic; + padding-top: 5px; + padding-bottom: 5px; + align-self: flex-end; + } +} + +pre { + background-color: $light; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.spec.ts new file mode 100644 index 00000000..0b56a57a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ConfigureComponent } from './configure.component'; + +describe('ConfigureComponent', () => { + let component: ConfigureComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ConfigureComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ConfigureComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.ts new file mode 100644 index 00000000..c6b4b48a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/configure/configure.component.ts @@ -0,0 +1,136 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ApiClient, HttpClient } from "../../../utils/HttpClient"; +import { FindIncidents, FindIncidentsResult } from "../../../../server-api/Core/Incidents"; +import { ApplicationService } from "../../application.service"; +declare var window: any; + +interface ILibrarySummary { + clientFolderName: string; + description: string; + id: string; + selected: boolean; + + + /** ".net" or "js" */ + frameworkType: string; + + categories: string[]; +} + +@Component({ + selector: 'app-configure', + templateUrl: './configure.component.html', + styleUrls: ['./configure.component.scss'] +}) +export class ConfigureComponent implements OnInit { + private lastLib: ILibrarySummary; + libraries: ILibrarySummary[] = []; + allLibraries: ILibrarySummary[] = []; + instruction: string | null = null; + frameworkNames: string[] = []; + selectedFramework = ""; + getStartedVisible = true; + + applicationId = 0; + appKey = ""; + sharedSecret = ""; + reportUrl = ""; + + noConnection = false; + weAreInTrouble = false; + + constructor( + private apiClient: ApiClient, + private httpClient: HttpClient, + private applicationService: ApplicationService, + activatedRoute: ActivatedRoute) { + this.applicationId = activatedRoute.snapshot.params["applicationId"]; + } + + ngOnInit(): void { + if (!this.applicationId) { + this.applicationService.list().then(x => { + if (x.length > 0) { + this.applicationId = x[0].id; + } + }); + } else { + this.load(); + } + + } + + @Input() + set showGetStarted(value: boolean) { + this.getStartedVisible = value; + } + + async load(): Promise { + var app = await this.applicationService.get(this.applicationId); + this.appKey = app.appKey; + this.sharedSecret = app.sharedSecret; + + var response = await this.httpClient.get('/api/onboarding/libraries/'); + try { + var data = response.body; + data.forEach(lib => { + lib.selected = false; + this.allLibraries.push(lib); + if (!this.frameworkNames.includes(lib.frameworkType)) { + this.frameworkNames.push(lib.frameworkType); + } + }); + } + catch (error) { + this.noConnection = true; + } + + var pos = window.location.href.toString().indexOf('/discover/'); + this.reportUrl = window.location.href.toString().substr(0, pos + 1); + } + + async selectFramework(name: string): Promise { + this.selectedFramework = name; + this.libraries = this.allLibraries + .filter(x => x.frameworkType === name) + .sort((x, y) => { + return x.id.localeCompare(y.id); + }); + } + + async selectLibrary(lib: ILibrarySummary): Promise { + this.lastLib = lib; + var url = '/api/onboarding/library/' + lib.id + "/?appKey=" + this.appKey + "&type=" + lib.frameworkType; + var response = await this.httpClient.get(url); + + this.instruction = response.body + .replace('yourAppKey', this.appKey) + .replace('yourSharedSecret', this.sharedSecret); + } + + tagStr(lib: ILibrarySummary) { + return lib.categories.join(', '); + } + + completedConfiguration() { + var q = new FindIncidents(); + q.pageNumber = 1; + q.itemsPerPage = 1; + q.applicationIds = [this.applicationId]; + this.apiClient.query(q) + .then(result => { + if (result.totalCount === 0) { + this.weAreInTrouble = true; + return; + } + + //this.$router.push({ + // name: "discover", + // params: { applicationId: this.applicationId.toString() } + //}); + }); + + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.html new file mode 100644 index 00000000..d1393d89 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.html @@ -0,0 +1 @@ +

edit works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.spec.ts new file mode 100644 index 00000000..34ef921a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditComponent } from './edit.component'; + +describe('EditComponent', () => { + let component: EditComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ EditComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(EditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.ts new file mode 100644 index 00000000..748a28a5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/edit/edit.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.scss'] +}) +export class EditComponent implements OnInit { + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.html new file mode 100644 index 00000000..01e3467f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.html @@ -0,0 +1,24 @@ +
+

Edit environment

+
+
+
+ + {{environment.name}} +
+ +
+ + + +
+ Do not store or analyze error reports in this environment. +
+ +
+
+ + +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.spec.ts new file mode 100644 index 00000000..5a24d45a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditComponent } from './edit.component'; + +describe('EditComponent', () => { + let component: EditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EditComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.ts new file mode 100644 index 00000000..9217da66 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/edit/edit.component.ts @@ -0,0 +1,60 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ApplicationService } from "../../../application.service"; +import { ActivatedRoute, Router } from "@angular/router"; +import { NavMenuService } from "../../../../nav-menu/nav-menu.service"; +import { EnvironmentService, Environment } from "../environment.service"; + +@Component({ + selector: 'app-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.scss'] +}) +export class EditComponent implements OnInit, OnDestroy { + environment: Environment = new Environment(-1, ''); + + private id = 0; + private applicationId = 0; + private sub: any; + + constructor(private appService: ApplicationService, + private service: EnvironmentService, + private route: ActivatedRoute, + private menuService: NavMenuService, + private router: Router) { } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.applicationId = +params['applicationId']; + this.id = +params['id']; + + this.appService.get(this.applicationId).then(x => { + + this.menuService.updateNav([ + { title: x.name, route: ['application', x.id] }, + { title: "Administration", route: ['application', x.id, 'admin'] }, + { title: "Edit Environment", route: ['application', x.id, 'admin', 'environments/', this.id] } + ]); + + }); + + this.load(); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + async save(): Promise { + await this.service.update(this.environment); + this.router.navigate(['/application', this.applicationId, 'admin']); + } + + cancel() { + this.router.navigate(['/application', this.applicationId, 'admin']); + } + + private async load(): Promise { + this.environment = await this.service.get(this.applicationId, this.id); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environment.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environment.service.spec.ts new file mode 100644 index 00000000..25893bf4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environment.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { EnvironmentService } from './environment.service'; + +describe('EnvironmentService', () => { + let service: EnvironmentService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EnvironmentService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environment.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environment.service.ts new file mode 100644 index 00000000..8ebe49d3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environment.service.ts @@ -0,0 +1,125 @@ +import { Injectable } from '@angular/core'; +import * as api from "../../../../server-api/Core/Environments"; +import { ApiClient } from "../../../utils/HttpClient"; +import { required, copy, validate } from "../../../validation"; + +export class Environment { + constructor(applicationId: number, name: string) { + this.applicationId = applicationId; + this.name = name; + } + + id?: number = null; + + @required + applicationId: number; + + + @required + name: string = ""; + + @required + ignoreErrorReports: boolean = false; +} + + +@Injectable({ + providedIn: 'root' +}) +export class EnvironmentService { + private environments = new Map(); + private loadPromises = new Map>(); + + constructor(private apiClient: ApiClient) { + + } + + async list(applicationId: number): Promise { + var promise = this.loadPromises.get(applicationId); + if (promise) { + return await promise; + } + + promise = this.listInner(applicationId); + this.loadPromises.set(applicationId, promise); + + this.environments.set(applicationId, await promise); + return this.environments.get(applicationId); + } + + async get(applicationId: number, id: number) { + var envs = await this.list(applicationId); + + var entry = envs.find(x => x.id === id); + if (!entry) { + throw new Error("Entry with id " + id + "was not found"); + } + + return entry; + } + + async create(env: Environment) { + if (!env) { + throw new Error("Environment must be specified."); + } + + var errors = validate(env); + if (errors.length > 0) { + throw new Error(errors.join(', ')); + } + + var cmd = new api.CreateEnvironment(); + copy(env, cmd); + await this.apiClient.command(cmd); + + var query = new api.GetEnvironments(); + query.applicationId = env.applicationId; + var dtos = await this.apiClient.query(query); + + var dto = dtos.items.find(x => x.name === env.name); + env.id = dto.id; + + var envs = await this.list(env.applicationId); + envs.push(env); + } + + async update(updated: Environment) { + var envs = await this.list(updated.applicationId); + var env = envs.find(x => x.name === updated.name); + + env.ignoreErrorReports = updated.ignoreErrorReports; + + var cmd = new api.UpdateEnvironment(); + cmd.applicationId = updated.applicationId; + cmd.deleteIncidents = updated.ignoreErrorReports; + cmd.environmentId = updated.id; + await this.apiClient.command(cmd); + } + + async reset(applicationId: number, environmentId: number) { + if (!applicationId) { + throw new Error("ApplicationId must be specified"); + } + + if (!environmentId) { + throw new Error("EnvironmentId must be specified"); + } + + var cmd = new api.ResetEnvironment(); + cmd.applicationId = applicationId; + cmd.environmentId = environmentId; + await this.apiClient.command(cmd); + } + + private async listInner(applicationId: number): Promise { + var query = new api.GetEnvironments(); + query.applicationId = applicationId; + var envs = await this.apiClient.query(query); + return envs.items.map(x => { + var e = new Environment(applicationId, x.name); + e.id = x.id; + e.ignoreErrorReports = x.deleteIncidents; + return e; + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environments.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environments.module.ts new file mode 100644 index 00000000..563c8f53 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/environments.module.ts @@ -0,0 +1,31 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { CommonModule } from '@angular/common'; + +import { NewComponent } from './new/new.component'; +import { EditComponent } from './edit/edit.component'; +import { EnvironmentListComponent } from './list/list.component'; + +const routes: Routes = [ + { path: 'application/:applicationId/admin/environments/:id/edit', component: EditComponent }, + { path: 'application/:applicationId/admin/environments/new', component: NewComponent } +]; + + +@NgModule({ + declarations: [ + NewComponent, + EditComponent, + EnvironmentListComponent + ], + imports: [ + CommonModule, + FormsModule, + RouterModule.forChild(routes) + ], + exports: [ + EnvironmentListComponent + ] +}) +export class EnvironmentsModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.html new file mode 100644 index 00000000..056b6b1e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.html @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + +
NameStore error reports 
{{env.name}}{{env.ignoreErrorReports?'no':'yes'}}
No environments have been created.
+ diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.spec.ts new file mode 100644 index 00000000..3c50756e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EnvironmentListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: EnvironmentListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [EnvironmentListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EnvironmentListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.ts new file mode 100644 index 00000000..876fd270 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/list/list.component.ts @@ -0,0 +1,42 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { EnvironmentService, Environment } from "../environment.service"; +import { ApplicationService } from "../../../application.service"; +import { ToastrService } from "ngx-toastr"; + +@Component({ + selector: 'env-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'] +}) +export class EnvironmentListComponent implements OnInit, OnDestroy { + private sub: any; + private applicationId: number; + environments: Environment[] = []; + + constructor(private service: EnvironmentService, + private appService: ApplicationService, + private route: ActivatedRoute, + private noticeService: ToastrService) { } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.applicationId = +params['applicationId']; + this.load(); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + deleteErrors(env: Environment) { + this.service.reset(env.id, env.applicationId); + this.noticeService.success("Environment has been queued to be reset."); + } + + private async load(): Promise { + this.environments = await this.service.list(this.applicationId); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.html new file mode 100644 index 00000000..f8c3a4fa --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.html @@ -0,0 +1,25 @@ +
+

New environment

+
+
+
+ + + Name specified as 'Err.Configuration.EnvironmentName` in the client library. +
+ +
+ + + +
+ Do not store or analyze error reports in this environment. +
+ +
+
+ + +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.spec.ts new file mode 100644 index 00000000..0607a22e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NewComponent } from './new.component'; + +describe('NewComponent', () => { + let component: NewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.ts new file mode 100644 index 00000000..fbf996d7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/environments/new/new.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from "@angular/router"; + +import { Environment, EnvironmentService } from "../environment.service"; +import { NavMenuService } from "../../../../nav-menu/nav-menu.service"; +import { ApplicationService } from "../../../application.service"; + +@Component({ + selector: 'app-new', + templateUrl: './new.component.html', + styleUrls: ['./new.component.scss'] +}) +export class NewComponent implements OnInit { + ignoreErrorReports = false; + name = ""; + private applicationId = 0; + private sub: any; + + constructor(private service: EnvironmentService, + private route: ActivatedRoute, + private router: Router, + private appService: ApplicationService, + private menuService: NavMenuService) { + + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.applicationId = +params['applicationId']; + + this.appService.get(this.applicationId).then(x => { + + this.menuService.updateNav([ + { title: x.name, route: ['application', x.id] }, + { title: "Administration", route: ['application', x.id, 'admin'] }, + { title: "New environment", route: ['application', x.id, 'admin', 'environments/'] } + ]); + + }); + + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + async save(): Promise { + var env = new Environment(this.applicationId, this.name); + env.ignoreErrorReports = this.ignoreErrorReports; + + await this.service.create(env); + this.router.navigate(['/application', this.applicationId, 'admin']); + } + + async cancel(): Promise { + this.router.navigate(['/application', this.applicationId, 'admin']); + } + + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.html new file mode 100644 index 00000000..3bcbb44b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.html @@ -0,0 +1,40 @@ +
+

Application administration

+
+
+

Application configuration

+
+
+ AppKey {{appKey}}
+ Shared secret {{sharedSecret}}
+
+

To get help configuring the Coderr libraries, click on the button below.

+ Configure +
+
+
+

Team

+ +
+
+

Partitions

+
+

Partitions are used to let Coderr prioritize errors based on your own business metrics.

+ + +
+
+
+

Environments

+
+

Track errors in different environments.

+ + +
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.scss new file mode 100644 index 00000000..4cabe639 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.scss @@ -0,0 +1,10 @@ +.panels { + /*align-items: stretch;*/ + grid-auto-rows: 1fr +} +.panel { + +} +.btn { + align-self: flex-end; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.spec.ts new file mode 100644 index 00000000..d81904fb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AdminHomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: AdminHomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AdminHomeComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AdminHomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.ts new file mode 100644 index 00000000..78fe0e7d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/home/home.component.ts @@ -0,0 +1,51 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ApplicationService } from "../../application.service"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; + +@Component({ + selector: 'app-admin-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class AdminHomeComponent implements OnInit, OnDestroy { + private sub: any; + applicationId: number; + sharedSecret = ''; + appKey = ''; + + framework = ''; + lib = ''; + + constructor( + private service: ApplicationService, + private route: ActivatedRoute, + private menuService: NavMenuService) { + this.applicationId = +route.snapshot.params.applicationId; + this.service.get(this.applicationId).then(x => { + this.sharedSecret = x.sharedSecret; + this.appKey = x.appKey; + }); + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.applicationId = +params['applicationId']; + this.service.get(this.applicationId).then(x => { + this.sharedSecret = x.sharedSecret; + this.appKey = x.appKey; + this.menuService.updateNav([ + { title: x.name, route: ['application', x.id] }, + { title: "Administration", route: ['application', x.id, 'admin'] } + ] + ); + + }); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.html new file mode 100644 index 00000000..4dcff9e8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.html @@ -0,0 +1,49 @@ +
+

Manage members

+ + + + + + + + + + + + + + + +
NamePermission 
+ {{member.userName}} + + {{member.isAdmin?'Administrator': member.isInvited ? 'Invited' : 'Member'}} + + + + +
+
+ +
+
+ + +
+

Add team member

+
+ + +
+ + +
+ + +
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.spec.ts new file mode 100644 index 00000000..12810275 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TeamListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: TeamListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [TeamListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TeamListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.ts new file mode 100644 index 00000000..dc489ca6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/list/list.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { ApplicationService } from "../../../application.service"; +import { AccountService, User } from "../../../../accounts/account.service"; +import { IApplicationMember } from "../../../application.model"; +import { ModalService } from "../../../../_controls/modal/modal.service"; +import { ToastrService } from "ngx-toastr"; +import { NavMenuService } from "../../../../nav-menu/nav-menu.service"; +import { AuthorizeService } from "../../../../../api-authorization/authorize.service"; + +@Component({ + selector: 'app-team', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'] +}) +export class TeamListComponent implements OnInit { + applicationId: number; + members: IApplicationMember[] = []; + users: User[] = []; + + //invitations: + selectedAccountId: number = -1; + inviteEmail: string = ""; + isAdmin: boolean; + + constructor(private service: ApplicationService, + private accountService: AccountService, + private authService: AuthorizeService, + route: ActivatedRoute, + private modalService: ModalService, + private noticeService: ToastrService, + private menuService: NavMenuService) { + this.applicationId = +route.snapshot.params.applicationId; + this.load(); + } + + ngOnInit(): void { + } + + showAdd() { + this.modalService.open("AddUserModel"); + } + + hideShowAdd() { + this.modalService.close("AddUserModel"); + } + + promote(user: IApplicationMember) { + (user).isAdmin = true; + this.service.makeAdmin(this.applicationId, user.accountId); + } + + demote(user: IApplicationMember) { + (user).isAdmin = false; + this.service.removeAdmin(this.applicationId, user.accountId); + } + + remove(user: IApplicationMember) { + this.service.removeMember(this.applicationId, user.accountId); + this.members = this.members.filter(x => x.accountId !== user.accountId); + } + + async addUser(): Promise { + this.hideShowAdd(); + + if (this.inviteEmail.length > 0) { + await this.service.inviteUser(this.applicationId, this.inviteEmail); + this.members.push({ accountId: -1, isAdmin: false, userName: this.inviteEmail, isInvited: true }); + this.inviteEmail = ''; + + } else if (this.selectedAccountId > 0) { + + this.selectedAccountId = +this.selectedAccountId; + + await this.service.addMember(this.applicationId, this.selectedAccountId, false); + this.members.push({ + accountId: this.selectedAccountId, + isAdmin: false, + userName: this.users.find(x => x.id === this.selectedAccountId).userName, + isInvited: false + }); + } + } + + private async load(): Promise { + this.users = await this.accountService.getAll(); + this.members = await this.service.getMembers(this.applicationId); + this.isAdmin = this.authService.user.isSysAdmin || + this.members.find(x => x.accountId === this.authService.user.accountId && x.isAdmin) != null; + + var app = await this.service.get(this.applicationId); + this.menuService.updateNav([ + { title: app.name, route: ['application', this.applicationId] }, + { title: "Administration", route: ['application', this.applicationId, 'admin'] }, + { title: "Team", route: ['application', this.applicationId, 'admin', 'team'] } + ]); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.html new file mode 100644 index 00000000..19f364f3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.html @@ -0,0 +1,5 @@ +
    +
  • {{member.userName}}
  • +
+ +Manage members diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.spec.ts new file mode 100644 index 00000000..675244fd --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MembersComponent } from './members.component'; + +describe('MembersComponent', () => { + let component: MembersComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MembersComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MembersComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.ts new file mode 100644 index 00000000..86f4889b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/members.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ApplicationService } from "../../application.service"; +import { IApplicationMember } from "../../application.model"; + +@Component({ + selector: 'app-members', + templateUrl: './members.component.html', + styleUrls: ['./members.component.scss'] +}) +export class MembersComponent implements OnInit { + id: number; + members: IApplicationMember[] = []; + + constructor( + private service: ApplicationService, + route: ActivatedRoute) { + + this.id = +route.snapshot.params.applicationId; + this.service.getMembers(this.id).then(x => { + this.members = x; + }); + } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.html new file mode 100644 index 00000000..339e5ede --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.html @@ -0,0 +1 @@ +

new works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.spec.ts new file mode 100644 index 00000000..0607a22e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NewComponent } from './new.component'; + +describe('NewComponent', () => { + let component: NewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.ts new file mode 100644 index 00000000..5eafa4db --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/members/new/new.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-new', + templateUrl: './new.component.html', + styleUrls: ['./new.component.scss'] +}) +export class NewComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.html new file mode 100644 index 00000000..b664b291 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.html @@ -0,0 +1,59 @@ +
+

New partitions

+
+
+
+

Basic information

+
+

Information required so that Coderr can prioritize errors based on your own criteria.

+
+ + + Name to show in the error information page. For instance "Users". +
+ +
+ + + Attribute value in the telemetry that is attached to the error reports. For instance 'UserId'. +
+ +
+ + + How many items are there? Used to be able to show percentage, i.e. "20% of the users are affected". +
+ +
+
+
+

Prioritization

+

Information required so that Coderr can prioritize, escalate and notify you when severe errors are detected.

+ +
+ + + How important is this partition relative to all other partitions? For instance, if one VIP customer is as important as 100 normal customers, add 100 here. +
+ + +
+ + + How many items must be affected for an error to be deemed as important? +
+ +
+ + + How many items must be affected for an error to be deemed as critical/severe? +
+ +
+
+
+ + +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.spec.ts new file mode 100644 index 00000000..1787117e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PartitionEditComponent } from './edit.component'; + +describe('EditComponent', () => { + let component: PartitionEditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PartitionEditComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PartitionEditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.ts new file mode 100644 index 00000000..4b0687fe --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/edit/edit.component.ts @@ -0,0 +1,72 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ApplicationService } from "../../../application.service"; +import { ActivatedRoute, Router } from "@angular/router"; +import { NavMenuService } from "../../../../nav-menu/nav-menu.service"; +import { PartitionService, Partition } from "../partition.service"; +import { copy } from "../../../../validation"; + +@Component({ + selector: 'partition-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.scss'] +}) +export class PartitionEditComponent implements OnInit { + private applicationId = 0; + private id = 0; + private sub: any; + private backendPartition: Partition; + partition: Partition = new Partition(); + + constructor(private appService: ApplicationService, + private partitionService: PartitionService, + private route: ActivatedRoute, + private menuService: NavMenuService, + private router: Router) { } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.applicationId = +params['applicationId']; + this.id = +params['id']; + this.load(); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + async save(): Promise { + copy(this.partition, + this.backendPartition, + { + stringFields: ['name', 'partitionKey'], + numericFields: ['weight', 'numberOfItems', 'importantThreshold', 'criticalThreshold'], + skipExistenceCheck: true + }); + + await this.partitionService.update(this.backendPartition); + + this.router.navigate(['/application', this.applicationId, 'admin']); + + } + + cancel() { + this.router.navigate(['/application', this.applicationId, 'admin']); + } + + private async load(): Promise { + var app = await this.appService.get(this.applicationId); + this.backendPartition = await this.partitionService.get(this.id); + copy(this.backendPartition, this.partition); + + + this.menuService.updateNav([ + { title: app.name, route: ['application', app.id] }, + { title: "Administration", route: ['application', app.id, 'admin'] }, + { title: "Edit partition", route: ['application', app.id, 'admin', 'partitions', this.id, 'edit'] } + ] + ); + + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.html new file mode 100644 index 00000000..83dc8470 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + +
NameTelemetry keyWeight
{{partition.name}}{{partition.partitionKey}}{{partition.weight}}
No partitions have been created.
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.spec.ts new file mode 100644 index 00000000..3aaad19b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PartitionListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: PartitionListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PartitionListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PartitionListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.ts new file mode 100644 index 00000000..bc074977 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/list/list.component.ts @@ -0,0 +1,37 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from "@angular/router"; +import { PartitionService, IPartitionListItem } from "../partition.service"; + +@Component({ + selector: 'partition-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'] +}) +export class PartitionListComponent implements OnInit, OnDestroy { + applicationId: number; + partitions: IPartitionListItem[] = []; + + private sub: any; + + constructor(private service: PartitionService, + private route: ActivatedRoute) { + + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.applicationId = +params['applicationId']; + this.loadEverything(); + }); + + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + async loadEverything(): Promise { + this.partitions = await this.service.listForApplication(this.applicationId); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.html new file mode 100644 index 00000000..7170011d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.html @@ -0,0 +1,59 @@ +
+

New partition

+

Create a new partition to allow Coderr to prioritize errors for you. Once you've added it here, just make sure that the field is transported along error reprots.

+
+
+

Basic information

+
+

Information required so that Coderr can prioritize errors based on your own criteria.

+
+ + + Name to show in the error information page. For instance "Users". +
+ +
+ + + Attribute value in the telemetry that is attached to the error reports. For instance 'UserId'. +
+ +
+ + + How many items are there? Used to be able to show percentage, i.e. "20% of the users are affected". +
+ +
+
+
+

Prioritization

+
+

Information required so that Coderr can prioritize, escalate and notify you when severe errors are detected.

+ +
+ + + How important is this partition relative to all other partitions? For instance, if one VIP customer is as important as 100 normal customers, add 100 here. +
+ + +
+ + + How many items must be affected for an error to be deemed as important? +
+ +
+ + + How many items must be affected for an error to be deemed as critical/severe? +
+
+
+
+
+ + +
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.spec.ts new file mode 100644 index 00000000..be4f7aa2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PartitionNewComponent } from './new.component'; + +describe('NewComponent', () => { + let component: PartitionNewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [PartitionNewComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(PartitionNewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.ts new file mode 100644 index 00000000..11d176bb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/new/new.component.ts @@ -0,0 +1,68 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ApplicationService } from "../../../application.service"; +import { ActivatedRoute, Router } from "@angular/router"; +import { NavMenuService } from "../../../../nav-menu/nav-menu.service"; +import { PartitionService } from "../partition.service"; + +@Component({ + selector: 'partition-new', + templateUrl: './new.component.html', + styleUrls: ['./new.component.scss'] +}) +export class PartitionNewComponent implements OnInit, OnDestroy { + weight = 0; + name = ''; + partitionKey = ''; + + numberOfItems?= null; + importantThreshold?= null; + criticalThreshold?= null; + + private id = 0; + private sub: any; + + constructor(private appService: ApplicationService, + private partitionService: PartitionService, + private route: ActivatedRoute, + private menuService: NavMenuService, + private router: Router) { } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.id = +params['applicationId']; + this.appService.get(this.id).then(x => { + + this.menuService.updateNav([ + { title: x.name, route: ['application', x.id] }, + { title: "Administration", route: ['application', x.id, 'admin'] }, + { title: "New partition", route: ['application', x.id, 'admin', 'partitions/new'] } + ] + ); + + }); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + async save(): Promise { + await this.partitionService.create({ + name: this.name, + partitionKey: this.partitionKey, + numberOfItems: this.numberOfItems, + importantThreshold: this.importantThreshold, + criticalThreshold: this.criticalThreshold, + applicationId: this.id, + weight: this.weight, + id: 0 + }); + this.router.navigate(['/application', this.id, 'admin']); + + } + + cancel() { + this.router.navigate(['/application', this.id, 'admin']); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partition.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partition.service.spec.ts new file mode 100644 index 00000000..2cac30b8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partition.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { PartitionService } from './partition.service'; + +describe('PartitionService', () => { + let service: PartitionService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(PartitionService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partition.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partition.service.ts new file mode 100644 index 00000000..97d32f62 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partition.service.ts @@ -0,0 +1,136 @@ +import { Injectable } from '@angular/core'; +import * as api from "../../../../server-api/Common/Partitions"; +import { ApiClient } from "../../../utils/HttpClient"; +import "../../../validation"; +import { required, stringLength, range2, copy, validate, range } from "../../../validation"; + +export interface IPartitionListItem { + id: number; + applicationId: number; + name: string; + partitionKey: string; + weight: number; +} + +export class Partition { + id = 0; + + @required + @range(1) + applicationId: number = 0; + + @required + @stringLength(40) + name: string = ''; + + @required + @stringLength(20) + partitionKey: string = ''; + + numberOfItems?: number = null; + weight: number = 1; + importantThreshold?: number = null; + criticalThreshold?: number = null; +} + +@Injectable({ + providedIn: 'root' +}) +export class PartitionService { + private listPromises = new Map>(); + private partitionPromises = new Map>(); + + constructor(private apiClient: ApiClient) { + + } + + + public async listForApplication(applicationId: number): Promise { + var promise = this.listPromises.get(applicationId); + if (promise != null) { + return await promise; + } + + var accept: any; + var reject: any; + this.listPromises[applicationId] = new Promise((acceptInner, rejectInner) => { + accept = acceptInner; + reject = rejectInner; + }); + + var query = new api.GetPartitions(); + query.applicationId = applicationId; + try { + var result = await this.apiClient.query(query); + + // Not supported in community edition. + if (!result) { + return null; + } + + var partitions: IPartitionListItem[] = result.items; + accept(partitions); + return partitions; + } catch (error) { + reject(error); + throw error; + } + } + + public async get(id: number): Promise { + var promise = this.partitionPromises.get(id); + if (promise != null) { + return await promise; + } + + var accept: any; + var reject: any; + this.partitionPromises[id] = new Promise((acceptInner, rejectInner) => { + accept = acceptInner; + reject = rejectInner; + }); + + var query = new api.GetPartition(); + query.id = id; + try { + var result = await this.apiClient.query(query); + + var partitions: Partition = result; + accept(partitions); + return partitions; + } catch (error) { + reject(error); + throw error; + } + } + + public async create(partition: Partition): Promise { + if (partition == null) { + throw new Error("Partition must be specified."); + } + var errors = validate(partition); + if (errors.length > 0) { + throw new Error(errors.join(',')); + } + + var cmd = new api.CreatePartition(); + copy(partition, cmd); + await this.apiClient.command(cmd); + } + + public async update(partition: Partition): Promise { + if (partition == null) { + throw new Error("Partition must be specified."); + } + + var errors = validate(partition); + if (errors.length > 0) { + throw new Error(errors.join(',')); + } + + var cmd = new api.UpdatePartition(); + copy(partition, cmd, {skipExistenceCheck: true}); + await this.apiClient.command(cmd); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partitions.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partitions.module.ts new file mode 100644 index 00000000..fa90d4b8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/admin/partitions/partitions.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { PartitionListComponent } from './list/list.component'; +import { PartitionEditComponent } from './edit/edit.component'; +import { PartitionNewComponent } from './new/new.component'; + + + +@NgModule({ + declarations: [PartitionListComponent, PartitionEditComponent, PartitionNewComponent], + imports: [ + CommonModule + ] +}) +export class PartitionsModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.model.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.model.spec.ts new file mode 100644 index 00000000..f7949354 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.model.spec.ts @@ -0,0 +1,7 @@ +import { IApplication } from './application.model'; + +describe('Application', () => { + it('should create an instance', () => { + //expect(new Application()).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.model.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.model.ts new file mode 100644 index 00000000..53967d3c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.model.ts @@ -0,0 +1,57 @@ +import { Observable } from 'rxjs'; + +export class Roles { + static Admin = "Admin"; + static Member = "Member"; +} + +export interface IApplicationListItem { + readonly id: number; + readonly name: string; +} + +export interface IApplication extends IApplicationListItem { + readonly members: IApplicationMember[]; + readonly groupIds: number[]; + readonly totalIncidentCount: number; + readonly latestIncidentDate?: Date; + readonly versions: string[]; + readonly sharedSecret: string; + readonly appKey: string; +} + +export class EmptyApplication implements IApplication { + id: number = 0; + name = ""; + members = []; + groupIds = [1]; + totalIncidentCount = 0; + versions = []; + appKey = ''; + sharedSecret = ''; +} + +export interface IApplicationMember { + /** + * -1 for invited that do not have an account + */ + readonly accountId: number; + readonly userName: string; + readonly isAdmin: boolean; + readonly isInvited: boolean; +} + +export interface IApplicationSummary { + readonly applicationId: number; + readonly name: string; +} + +export class Guid { + static newGuid() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + const r = Math.random() * 16 | 0, + v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.module.ts new file mode 100644 index 00000000..3849959f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.module.ts @@ -0,0 +1,97 @@ +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { CommonModule } from '@angular/common'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { PipeModule } from "../../pipes/pipe.module"; + +import { EditComponent } from './admin/edit/edit.component'; +import { MembersComponent } from './admin/members/members.component'; +import { GroupSelectorComponent } from "./groups/selector.component"; +import { EnvironmentsModule } from "./admin/environments/environments.module"; +import { SummaryChartComponent } from "./charts/summary/summary.component"; +import { InsightChartComponent } from "./charts/insights/insightchart.component"; +import { AppInsightsDashboardComponent } from './insights/dashboard/dashboard.component'; +import { AppInsightsDetailsComponent } from './insights/details/details.component'; + +import { ConfigureComponent } from './admin/configure/configure.component'; + +//import { IncidentsModule } from "../incidents/incidents.module"; +import { ApplicationDetailsComponent } from "./details/details.component"; +import { ApplicationHomeComponent } from "./details/home/home.component"; +import { NavbarComponent } from './details/navbar/navbar.component'; + +import { ControlsModule } from "../_controls/controls.module"; +import { IncidentsModule } from "../incidents/incidents.module"; +import { MetricComponent } from './charts/metric/metric.component'; + +import { AdminHomeComponent } from './admin/home/home.component'; +import { PartitionListComponent } from "./admin/partitions/list/list.component"; +import { PartitionEditComponent } from "./admin/partitions/edit/edit.component"; +import { PartitionNewComponent } from "./admin/partitions/new/new.component"; + +import { NewComponent } from './admin/members/new/new.component'; +import { TeamListComponent } from "./admin/members/list/list.component"; + +const ourRoutes: Routes = [ + { + path: 'application/:applicationId', + component: ApplicationDetailsComponent, + children: [ + { + path: '', + outlet: 'application-details-outlet', + component: ApplicationHomeComponent + } + ] + }, + { path: 'application/:applicationId/configure', component: ConfigureComponent }, + { path: 'application/:applicationId/members', component: MembersComponent }, + { path: 'application/:applicationId/edit', component: EditComponent }, + { path: 'application/:applicationId/insights', component: AppInsightsDashboardComponent }, + { path: 'application/:applicationId/admin', component: AdminHomeComponent }, + { path: 'application/:applicationId/admin/team', component: TeamListComponent }, + { path: 'application/:applicationId/admin/partitions/:id/edit', component: PartitionEditComponent }, + { path: 'application/:applicationId/admin/partitions/new', component: PartitionNewComponent } +]; + +@NgModule({ + declarations: [ + ConfigureComponent, + GroupSelectorComponent, + EditComponent, + MembersComponent, + SummaryChartComponent, + InsightChartComponent, + ApplicationDetailsComponent, + ApplicationHomeComponent, + NavbarComponent, + AppInsightsDashboardComponent, + AppInsightsDetailsComponent, + MetricComponent, + AdminHomeComponent, + PartitionListComponent, + PartitionEditComponent, + PartitionNewComponent, + TeamListComponent, + NewComponent + ], + imports: [ + PipeModule, + CommonModule, + ControlsModule, + EnvironmentsModule, + FormsModule, + IncidentsModule, + ReactiveFormsModule, + RouterModule.forChild(ourRoutes) + ], + exports: [ + GroupSelectorComponent, + SummaryChartComponent, + InsightChartComponent, + MembersComponent, + PartitionListComponent, + ConfigureComponent + ] +}) +export class ApplicationModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.service.spec.ts new file mode 100644 index 00000000..b6fc8dcc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { ApplicationService as ApplicationsService } from './application.service'; + +describe('ApplicationService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: ApplicationsService = TestBed.get(ApplicationsService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.service.ts new file mode 100644 index 00000000..2ba014bf --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/application.service.ts @@ -0,0 +1,435 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { BehaviorSubject, Subject } from 'rxjs'; +import * as model from "./application.model"; +import * as api from "../../server-api/Core/Applications"; +import * as inviteApi from "../../server-api/Core/Invitations"; +import * as incidentApi from "../../server-api/Core/Incidents"; +import { AuthorizeService, IUser } from "../../api-authorization/authorize.service"; +import { BehaviorSubjectList } from "../utils/SubjectList"; +import { ApiClient } from "../utils/HttpClient"; +import { SignalRService, ISubscriber, IHubEvent } from "../services/signal-r.service"; +import { ApplicationGroupService } from "../admin/groups/application-groups.service"; +import { PromiseWrapper } from "../PromiseWrapper"; +import Roles = model.Roles; + +@Injectable({ + providedIn: 'root' +}) +export class ApplicationService implements ISubscriber, OnDestroy { + private apps = new BehaviorSubjectList((a, b) => a.name.localeCompare(b.name)); + selected: BehaviorSubject; + updated: BehaviorSubject; + private timers: any[] = []; + private destroyed = false; + private selectedId: number = -1; + private loadPromise: PromiseWrapper = new PromiseWrapper(); + private userSub: any; + private isLoadingApps = false; + private isLoggedIn = false; + + constructor( + private client: ApiClient, + private authService: AuthorizeService, + private groupService: ApplicationGroupService, + private signalHub: SignalRService) { + + this.userSub = this.authService.userEvents.subscribe(user => this.onAuthenticated(user)); + + this.selected = new BehaviorSubject(null); + + signalHub.subscribe(x => { + return x.typeName === "IncidentCreated" || x.typeName === "IncidentClosed" || x.typeName === "IncidentIgnored"; + }, this); + + + } + + get applications(): Subject { + return this.apps.subject; + } + + + async create(name: string, groupId?: number, appKey?: string): Promise { + var existingApp = this.apps.current.find(x => x.name === name); + if (existingApp) { + return existingApp; + } + + var cmd = new api.CreateApplication(); + cmd.name = name; + if (appKey) { + cmd.applicationKey = appKey; + } else { + cmd.applicationKey = model.Guid.newGuid().replace(/\-/g, ''); + } + if (groupId) { + cmd.groupId = groupId; + } + await this.client.command(cmd); + + var event = await this.signalHub.wait(x => + x.typeName === "ApplicationCreated" && + x.body.createdById === this.authService.user.accountId); + + // Double check so it wasn't added by another thread + existingApp = this.apps.current.find(x => x.name === name); + if (existingApp) { + return existingApp; + } + + var members: model.IApplicationMember[] = [ + { + accountId: this.authService.user.accountId, + userName: this.authService.user.userName, + isAdmin: true, + isInvited: false + } + ]; + + var groups = cmd.groupId ? [cmd.groupId] : []; + var app = new Application(event.body.applicationId, cmd.name, groups, true, members); + + this.apps.add(app); + return app; + } + + async selectApplication(applicationId: number) { + if (applicationId == null || applicationId < 0) + throw new Error("Must supply an application id"); + if (this.selectedId === applicationId) { + return; + } + + if (applicationId === 0) { + this.selected.next(null); + } else { + const app = await this.get(applicationId); + this.selected.next(app); + } + } + + async get(applicationId: number): Promise { + if (!applicationId) { + throw new Error("ApplicationId must be specified."); + } + if (!this.isLoggedIn) { + return null; + } + + // guard against strings :/ + applicationId = +applicationId; + + await this.loadPromise; + + let existingApp = this.apps.find(x => x.id === applicationId); + if (existingApp != null && existingApp.latestIncidentDate != null) { + return existingApp; + } + + const query = new api.GetApplicationInfo(); + query.applicationId = applicationId; + const result = await this.client.query(query); + + let loadedApp: Application; + if (existingApp == null) { + var groups = await this.groupService.getGroupsForApplication(applicationId); + loadedApp = new Application(result.id, result.name, groups, false); + } else { + loadedApp = existingApp; + } + + loadedApp.sharedSecret = result.sharedSecret; + loadedApp.appKey = result.appKey; + + loadedApp.totalIncidentCount = result.totalIncidentCount; + loadedApp.latestIncidentDate = result.lastIncidentAtUtc; + loadedApp.versions = result.versions; + + if (existingApp == null) { + existingApp = this.apps.find(x => x.id === applicationId); + if (existingApp) { + return existingApp; + } + + this.apps.add(loadedApp); + } + + return loadedApp; + } + + async makeAdmin(applicationId: number, userId: number): Promise { + var cmd = new api.UpdateRoles(); + cmd.userToUpdate = userId; + cmd.roles = [Roles.Admin, Roles.Member]; + cmd.applicationId = applicationId; + await this.client.command(cmd); + } + + async removeAdmin(applicationId: number, userId: number): Promise { + var cmd = new api.UpdateRoles(); + cmd.userToUpdate = userId; + cmd.roles = [Roles.Member]; + cmd.applicationId = applicationId; + await this.client.command(cmd); + } + + async getMembers(applicationId: number): Promise { + if (!applicationId) { + throw new Error("ApplicationId must be specified."); + } + + const query = new api.GetApplicationTeam(); + query.applicationId = applicationId; + const result = await this.client.query(query); + var members = result.members.map(x => { + return { + accountId: x.userId, + userName: x.userName, + isAdmin: x.isAdmin, + isInvited: false + } + }); + + result.invited.forEach(x => { + members.push({ + accountId: -1, + isAdmin: false, + isInvited: true, + userName: x.emailAddress + }); + }); + + return members; + } + + async inviteUser(applicationId: number, email: string, message?: string): Promise { + if (!applicationId) { + throw new Error("ApplicationId must be specified."); + } + + if (!email) { + throw new Error("email must be specified."); + } + + var cmd = new inviteApi.InviteUser(); + cmd.applicationId = applicationId; + cmd.emailAddress = email; + cmd.text = message; + await this.client.command(cmd); + } + + async addMember(applicationId: number, accountId: number, isAdmin: boolean): Promise { + if (!applicationId) { + throw new Error("ApplicationId must be specified."); + } + + if (!accountId) { + throw new Error("accountId must be specified."); + } + + var cmd = new api.AddTeamMember(); + cmd.applicationId = applicationId; + cmd.userToAdd = accountId; + await this.client.command(cmd); + } + + + async removeMember(applicationId: number, accountId: number): Promise { + if (!applicationId) { + throw new Error("ApplicationId must be specified."); + } + + if (!accountId) { + throw new Error("accountId must be specified."); + } + + var cmd = new api.RemoveTeamMember(); + cmd.applicationId = applicationId; + cmd.userToRemove = accountId; + await this.client.command(cmd); + } + + + async list(): Promise { + if (!this.isLoggedIn) { + return []; + } + + await this.loadPromise.promise; + return this.apps.current; + } + + ngOnDestroy() { + this.timers.forEach(timer => { + clearInterval(timer); + }); + this.signalHub.unsubscribe(this); + this.userSub.unsubscribe(this); + } + + handle(event: IHubEvent) { + this.handleAsync(event); + } + + private async handleAsync(event: IHubEvent): Promise { + switch (event.typeName) { + case incidentApi.IncidentCreated.TYPE_NAME: + const incidentCreated = event.body; + var app1 = this.apps.find(x => x.id === incidentCreated.applicationId); + if (app1 != null) { + (app1).totalIncidentCount++; + } + break; + + case incidentApi.IncidentClosed.TYPE_NAME: + const incidentClosed = event.body; + var app2 = this.apps.find(x => x.id === incidentClosed.applicationId); + if (app2 != null) { + (app2).totalIncidentCount--; + } + break; + + case incidentApi.IncidentIgnored.TYPE_NAME: + const incidentIgnored = event.body; + var app3 = this.apps.find(x => x.id === incidentIgnored.applicationId); + if (app3 != null) { + (app3).totalIncidentCount--; + } + break; + } + + } + + // Should only be invoked when the user is published. + private async loadApplicationsOnAuth(): Promise { + try { + + const query = new api.GetApplicationList(); + const result = await this.client.query(query); + + this.apps.clear(); + + var apps: Application[] = []; + + var groups = await this.groupService.list(); + + result.forEach(dto => { + var appGroups = groups.filter(x => x.applications.includes(dto.id)).map(x => x.id); + const app = new Application(dto.id, dto.name, appGroups, dto.isAdmin); + apps.push(app); + }); + this.apps.addAll(apps); + + // Have access to one or more applications. + // Have zero applications when registering manually (instead of invites). + if (result.length > 0) { + this.selectApplication(result[0].id); + } + + + this.loadPromise.accept(null); + this.isLoadingApps = false; + return this.apps; + + } catch (error) { + this.loadPromise.reject(error); + this.isLoadingApps = false; + return null; + } + } + + + private onAuthenticated(user: IUser) { + if (!user || this.isLoadingApps) { + if (!user) { + this.isLoggedIn = false; + this.loadPromise = new PromiseWrapper(); + } + return; + } + + this.isLoggedIn = true; + this.isLoadingApps = true; + this.loadApplicationsOnAuth(); + }; +} + + + +class Application implements model.IApplication { + private _members: model.IApplicationMember[]; + private _name: string; + private _groupIds = []; + private _totalIncidentCount = 0; + private _latestIncidentDate?: Date = null; + private _versions: string[] = []; + private _sharedSecret = ''; + private _appKey = ''; + + constructor(private _id: number, name: string, groupIds: number[], isAdmin?: boolean, members?: model.IApplicationMember[]) { + if (!_id) { + throw new Error("Id must be defined"); + } + + if (!name) { + throw new Error("name must be defined"); + } + + this._name = name; + if (members) { + this._members = members; + } else { + this._members = []; + } + this._groupIds = groupIds; + } + + get groupIds(): number[] { + return this._groupIds; + } + get id(): number { + return this._id; + } + get name(): string { + return this._name; + } + get sharedSecret(): string { + return this._sharedSecret; + } + set sharedSecret(value: string) { + this._sharedSecret = value; + } + get appKey(): string { + return this._appKey; + } + set appKey(value: string) { + this._appKey = value; + } + get members(): model.IApplicationMember[] { + return this._members; + } + get totalIncidentCount(): number { + return this._totalIncidentCount; + } + set totalIncidentCount(count: number) { + this._totalIncidentCount = count; + } + get latestIncidentDate(): Date { + return this._latestIncidentDate; + } + set latestIncidentDate(count: Date) { + this._latestIncidentDate = count; + } + get versions(): string[] { + return this._versions; + } + set versions(values: string[]) { + this._versions = values; + } + + + addMember(newMember: model.IApplicationMember) { + this._members.push(newMember); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.html new file mode 100644 index 00000000..f8d23891 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.html @@ -0,0 +1,6 @@ +
+ +
+
+ There is no data to process. +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.spec.ts new file mode 100644 index 00000000..2c81528a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InsightChartComponent } from './insightchart.component'; + +describe('InsightChartComponent', () => { + let component: InsightChartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [InsightChartComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InsightChartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.ts new file mode 100644 index 00000000..ade41bfb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/insights/insightchart.component.ts @@ -0,0 +1,110 @@ +import { Component, OnInit, OnDestroy, Input } from "@angular/core"; +import * as api from "../../../../server-api/Common/Partitions"; +import { ApiClient } from "../../../utils/HttpClient"; +import { ChartService, IChartSeries } from "../../../services/chart.service"; + +@Component({ + selector: "app-insightchart", + templateUrl: "./insightchart.component.html", + styleUrls: ["./insightchart.component.scss"] +}) +export class InsightChartComponent implements OnInit, OnDestroy { + private _applicationId: number; + private _resfreshSeconds = 10; + private _timer: any; + chartId = ""; + noData = false; + + constructor( + private apiClient: ApiClient, + private chartService: ChartService + ) { + this.chartId = this.chartService.generateChartId(); + } + + @Input() + get applicationId(): number { + return this._applicationId; + } + + set applicationId(applicationId: number) { + this._applicationId = applicationId; + this.loadStats(); + this.resetTimer(); + } + + @Input() + get refreshSeconds(): number { + return this._resfreshSeconds; + } + + set refreshSeconds(value: number) { + this._resfreshSeconds = value; + } + + private resetTimer() { + clearInterval(this._timer); + this._timer = setInterval(() => this.reloadStats(), this.refreshSeconds * 1000); + } + + private async reloadStats(): Promise { + + const query = new api.GetPartitionInsights(); + query.applicationIds = [this.applicationId]; + const result = await this.apiClient.query(query); + + // null = not supported. + if (!result || result.applications.length === 0) { + this.noData = true; + return null; + } + + var series: IChartSeries[] = []; + result.applications[0].indicators.forEach(indicator => { + series.push({ + name: indicator.displayName, + data: indicator.values + }); + }); + + this.chartService.updateLineChart(this.chartId, series); + return null; + } + + ngOnInit(): void { + this.resetTimer(); + } + + ngOnDestroy() { + clearInterval(this._timer); + } + + private async loadStats() { + const query = new api.GetPartitionInsights(); + query.applicationIds = [this.applicationId]; + const result = await this.apiClient.query(query); + + // null = not supported. + if (!result || result.applications.length === 0) { + if (!result) { + clearInterval(this._timer); + } + + this.noData = true; + return; + } + + var series: IChartSeries[] = []; + result.applications[0].indicators.forEach(indicator => { + series.push({ + name: indicator.displayName, + data: indicator.values + }); + }); + + const legend = this.chartService.generateLabelsFromStringDate(result.applications[0].indicators[0].dates); + this.chartService.drawLineChart(this.chartId, { labels: legend }, series); + }; + + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.html new file mode 100644 index 00000000..f8d23891 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.html @@ -0,0 +1,6 @@ +
+ +
+
+ There is no data to process. +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.spec.ts new file mode 100644 index 00000000..888ec529 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MetricComponent } from './metric.component'; + +describe('MetricComponent', () => { + let component: MetricComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MetricComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MetricComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.ts new file mode 100644 index 00000000..737e7952 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/metric/metric.component.ts @@ -0,0 +1,109 @@ +import { Component, OnInit, OnDestroy, Input } from "@angular/core"; +import * as api from "../../../../server-api/Common/Partitions"; +import { ApiClient } from "../../../utils/HttpClient"; +import { ChartService, IChartSeries } from "../../../services/chart.service"; + +@Component({ + selector: 'app-metric', + templateUrl: './metric.component.html', + styleUrls: ['./metric.component.scss'] +}) +export class MetricComponent implements OnInit, OnDestroy { + private _applicationId: number; + private _resfreshSeconds = 10; + private _timer: any; + chartId = ""; + noData = false; + + constructor( + private apiClient: ApiClient, + private chartService: ChartService + ) { + this.chartId = this.chartService.generateChartId(); + } + + @Input() + get applicationId(): number { + return this._applicationId; + } + + set applicationId(applicationId: number) { + this._applicationId = applicationId; + this.loadStats(); + this.resetTimer(); + } + + @Input() + get refreshSeconds(): number { + return this._resfreshSeconds; + } + + set refreshSeconds(value: number) { + this._resfreshSeconds = value; + } + + private resetTimer() { + clearInterval(this._timer); + this._timer = setInterval(() => this.reloadStats(), this.refreshSeconds * 1000); + } + + private async reloadStats(): Promise { + + const query = new api.GetPartitionInsights(); + query.applicationIds = [this.applicationId]; + const result = await this.apiClient.query(query); + + // null = not supported. + if (!result || result.applications.length === 0) { + this.noData = true; + return null; + } + + var series: IChartSeries[] = []; + result.applications[0].indicators.forEach(indicator => { + series.push({ + name: indicator.displayName, + data: indicator.values + }); + }); + + this.chartService.updateLineChart(this.chartId, series); + return null; + } + + ngOnInit(): void { + this.resetTimer(); + } + + ngOnDestroy() { + clearInterval(this._timer); + } + + private async loadStats() { + const query = new api.GetPartitionInsights(); + query.applicationIds = [this.applicationId]; + const result = await this.apiClient.query(query); + + // null = not supported. + if (!result || result.applications.length === 0) { + if (!result) { + clearInterval(this._timer); + } + + this.noData = true; + return; + } + + var series: IChartSeries[] = []; + result.applications[0].indicators.forEach(indicator => { + series.push({ + name: indicator.displayName, + data: indicator.values + }); + }); + + const legend = this.chartService.generateLabelsFromStringDate(result.applications[0].indicators[0].dates); + this.chartService.drawLineChart(this.chartId, { labels: legend }, series); + }; + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.html new file mode 100644 index 00000000..bb8ba56c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.spec.ts new file mode 100644 index 00000000..381ddc7d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SummaryChartComponent as SummarychartComponent } from './summary.component'; + +describe('SummaryChartComponent', () => { + let component: SummarychartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SummarychartComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SummarychartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.ts new file mode 100644 index 00000000..1dbd6d3c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/charts/summary/summary.component.ts @@ -0,0 +1,92 @@ +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import * as api from "../../../../server-api/Core/Applications"; +import { ApiClient } from "../../../utils/HttpClient"; +import { ChartService, IChartSeries } from "../../../services/chart.service"; + +@Component({ + selector: 'app-summarychart', + templateUrl: './summary.component.html', + styleUrls: ['./summary.component.scss'] +}) +export class SummaryChartComponent implements OnInit, OnDestroy { + private _applicationId: number; + private _refreshSeconds: number = 10; + private _timer: any; + chartId: string = "summaryChart"; + + constructor( + private apiClient: ApiClient, + private chartService: ChartService + ) { + this.chartId = this.chartService.generateChartId(); + } + + @Input() + get applicationId(): number { return this._applicationId; } + set applicationId(applicationId: number) { + this._applicationId = applicationId; + this.loadStats(); + } + + + @Input() + get refreshSeconds(): number { + return this._refreshSeconds; + } + + set refreshSeconds(value: number) { + this._refreshSeconds = value; + } + + private resetTimer() { + clearInterval(this._timer); + this._timer = setInterval(() => this.reloadStats(), this.refreshSeconds * 1000); + } + + private async reloadStats(): Promise { + return null; + var query = new api.GetApplicationOverview(); + query.applicationId = this.applicationId; + var result = await this.apiClient.query(query); + + var series: IChartSeries[] = []; + series.push({ + name: 'Distinct new errors', + data: result.incidents + }); + series.push({ + name: 'Received error reports', + data: result.errorReports + }); + + this.chartService.updateLineChart(this.chartId, series); + return null; + } + + ngOnInit(): void { + this.resetTimer(); + } + + ngOnDestroy() { + clearInterval(this._timer); + } + + private async loadStats() { + var query = new api.GetApplicationOverview(); + query.applicationId = this.applicationId; + var result = await this.apiClient.query(query); + + var series: IChartSeries[] = []; + series.push({ + name: 'Distinct new errors', + data: result.incidents + }); + series.push({ + name: 'Received error reports', + data: result.errorReports + }); + + this.chartService.drawLineChart(this.chartId, { labels: result.timeAxisLabels, tickCount: 15 }, series); + } +} + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.html new file mode 100644 index 00000000..c527e57a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.html @@ -0,0 +1,5 @@ + + +
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.scss new file mode 100644 index 00000000..f2c66279 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.scss @@ -0,0 +1,51 @@ +@import "../../../styles/_partials/coderr-variables.scss"; +@import "../../../styles/_partials/_mixins.scss"; + +.application-view { + max-width: 100%; + display: block; + margin: 0; + + h3 { + color: white; + @include text-shadow-1; + } + + .fill { + background: white; + padding: 10px; + border-radius: 2px; + @include box-shadow-1; + + th { + text-align: right; + } + } + + pre { + background-color: transparent; + overflow: hidden; + word-wrap: normal; + white-space: pre; + position: relative; + + > code { + background-color: transparent; + } + } + + pre:hover { + overflow: auto; + } + + select { + padding: 4px; + background-color: $light; + border: 1px solid darken($light, 10px); + border-radius: 5px; + } + + table tbody th { + min-width: 200px; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.spec.ts new file mode 100644 index 00000000..aa2f9e87 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ApplicationDetailsComponent } from './details.component'; + +describe('DetailsComponent', () => { + let component: ApplicationDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ApplicationDetailsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ApplicationDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.ts new file mode 100644 index 00000000..fdc804e8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/details.component.ts @@ -0,0 +1,55 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { AccountService, User } from "../../accounts/account.service"; +import { ChartService } from "../../services/chart.service"; +import { NavMenuService } from "../../nav-menu/nav-menu.service"; +import { ApplicationService } from "../../applications/application.service"; +import { IApplication } from "../application.model"; + +@Component({ + selector: 'app-details', + templateUrl: './details.component.html', + styleUrls: ['./details.component.scss'] +}) +export class ApplicationDetailsComponent implements OnInit, OnDestroy { + application: IApplication; + applicationId: number; + users: User[] = []; + + + private sub: any; + + constructor( + private applicationService: ApplicationService, + private route: ActivatedRoute, + private accountService: AccountService, + private menuService: NavMenuService, + ) { + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.applicationId = +params['applicationId']; + this.loadEverything(); + }); + + this.accountService.getAllButMe() + .then(users => { + this.users = users; + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + + private async loadEverything() { + this.application = await this.applicationService.get(this.applicationId); + this.menuService.updateNav([ + { title: this.application.name, route: ['application', this.applicationId] } + ] + ); + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.html new file mode 100644 index 00000000..66499ee2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.html @@ -0,0 +1,38 @@ +
+
+
+

My errors

+
+ +
+
+
+

Recommended

+
+ +
+
+
+

Latest

+
+ +
+
+ +
+
+
+

Summary

+
+ +
+
+
+

Business impact

+
+ +
+
+
+
+ diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.scss new file mode 100644 index 00000000..7bab6f3e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.scss @@ -0,0 +1 @@ +@import "../details.component.scss"; diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.spec.ts new file mode 100644 index 00000000..76c5e66d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ApplicationHomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: ApplicationHomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ApplicationHomeComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ApplicationHomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.ts new file mode 100644 index 00000000..2252f8a1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/home/home.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ApplicationService } from "../../application.service"; +import { ActivatedRoute } from "@angular/router"; +import { IApplication, EmptyApplication } from "../../application.model"; +import { NavMenuService } from "../../../nav-menu/nav-menu.service"; + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class ApplicationHomeComponent implements OnInit, OnDestroy { + app: IApplication = new EmptyApplication(); + applicationId: number = 0; + sub: any; + + constructor( + private readonly appService: ApplicationService, + private readonly navService: NavMenuService, + private readonly route: ActivatedRoute) { + + } + + ngOnInit(): void { + this.sub = this.route.parent.params.subscribe(params => { + this.applicationId = +params['applicationId']; + this.appService.get(this.applicationId) + .then(x => { + this.app = x; + this.navService.updateNav([{ + title: x.name, + route: ['application', x.id] + }]); + }); + }); + + } + ngOnDestroy(): void { + this.sub.unsubscribe(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.html new file mode 100644 index 00000000..fcf87c00 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.html @@ -0,0 +1,38 @@ + + + +

Application configuration

+
+

+ To install the Coderr client in your application, click on this icon and follow the configuration instructions. +

+

+ Under settings, you can also activate the automated error prioritization with the feature "Partitions". +

+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.scss new file mode 100644 index 00000000..0dd334b3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.scss @@ -0,0 +1,99 @@ +@import "../../../../styles/_partials/coderr-variables.scss"; +@import "../../../../styles/_partials/_mixins.scss"; + +.submenu { + color: #ddd; + background: $nav-sub-bg; + padding-left: 15px; + padding-right: 15px; + + .groups { + margin-top: 5px; + margin-bottom: 10px; + + a { + background-color: $nav-sub-tab; + padding: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + } + } + + a { + color: $nav-text; + text-decoration: none; + } + + .application-list { + display: flex; + flex-direction: column; + } + + .application-list div { + padding: 5px; + } + + .state { + margin-top: 10px; + padding-bottom: 10px; + + dl { + display: inline; + + dt { + display: inline; + color: #999; + margin-right: 5px; + } + + dd { + display: inline; + margin-left: 0; + margin-right: 10px; + + select { + border-radius: 3px; + padding: 2px; + background-color: rgba(0, 0, 0, 0.3); + border-color: rgba(255, 255, 255, 0.1); + color: #ddd; + } + } + } + + .tags { + display: inline; + + span { + border-radius: 2px; + padding: 2px; + font-size: 0.8em; + background-color: darken($blue, 40%); + margin-right: 5px; + } + } + } + + .actions { + margin-left: auto; + margin-bottom: 2px; + margin-top: auto; + + a { + color: darken($blue, 50%); + padding: 3px 5px; + border-top: 1px solid darken($blue, 10%); + border-left: 1px solid darken($blue, 10%); + border-right: 1px solid darken($blue, 10%); + background-color: $blue; + + &.active { + color: lighten($blue, 50%); + } + + &:hover { + color: $red; + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.spec.ts new file mode 100644 index 00000000..f8ccd6f4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NavbarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.ts new file mode 100644 index 00000000..10d0d77a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/details/navbar/navbar.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { AccountService, User } from "../../../accounts/account.service"; +import { EmptyApplication, IApplication } from "../../application.model"; +import { ApplicationService } from "../../application.service"; +import { SignalRService, ISubscriber, IHubEvent } from "../../../services/signal-r.service"; +import * as api from "../../../../server-api/Core/Incidents"; + +@Component({ + selector: 'application-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'] +}) +export class NavbarComponent implements ISubscriber, OnInit { + application: IApplication = new EmptyApplication(); + myApplicationId: number; + users: User[] = []; + incidentCount: number; + latestIncidentDate: Date; + + constructor( + private applicationService: ApplicationService, + private accountService: AccountService, + signalR: SignalRService) { + //signalR.subscribe(x => { + // return x.typeName === "IncidentCreated" || x.typeName === "IncidentClosed"; + //}, this); + } + + ngOnInit(): void { + } + + handle(event: IHubEvent) { + switch (event.typeName) { + case api.IncidentClosed.TYPE_NAME: + this.incidentCount--; + break; + + case api.IncidentCreated.TYPE_NAME: + var info = event.body; + this.incidentCount++; + this.latestIncidentDate = new Date(Date.parse(info.createdAtUtc)); + break; + } + } + + @Input() + get applicationId(): number { return this.myApplicationId; } + set applicationId(applicationId: number) { + this.myApplicationId = applicationId; + if (this.myApplicationId > 0) { + this.loadEverything(); + } + } + + + private async loadEverything() { + this.application = await this.applicationService.get(this.myApplicationId); + this.users = await this.accountService.getAll(); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.html new file mode 100644 index 00000000..a7dd7a06 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.html @@ -0,0 +1,6 @@ + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.scss new file mode 100644 index 00000000..ab5ffd4e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.scss @@ -0,0 +1,15 @@ +@import "../../../styles/_partials/coderr-variables.scss"; + +.pills { + display: flex; + flex: 0 0 1fr; +} + +.pill { + background: $blue; + color: white; + border-radius: 3px; + padding: 4px; + box-shadow: rgba(50, 50, 93, 0.25) 0px 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; + margin-right: 5px; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.spec.ts new file mode 100644 index 00000000..6b44fb21 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GroupSelectorComponent } from './selector.component'; + +describe('SelectorComponent', () => { + let component: GroupSelectorComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GroupSelectorComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GroupSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.ts new file mode 100644 index 00000000..659c1dcc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/groups/selector.component.ts @@ -0,0 +1,41 @@ +import { Component, OnInit, OnDestroy, Output, EventEmitter } from '@angular/core'; +import { IGroupListItem } from "../../admin/groups/group.model"; +import { ApplicationGroupService } from "../../admin/groups/application-groups.service"; + +@Component({ + selector: 'app-group-selector', + templateUrl: './selector.component.html', + styleUrls: ['./selector.component.scss'] +}) +export class GroupSelectorComponent implements OnInit, OnDestroy { + private sub: any; + groups: IGroupListItem[] = []; + + @Output() selected = new EventEmitter(); + + + constructor(private readonly service: ApplicationGroupService) { } + + select(id: number) { + if (id === -1) { + this.selected.emit(null); + } + + const groups = this.groups.filter(x => x.id === id); + if (groups.length !== 1) { + throw new Error("Found multiple groups: " + JSON.stringify(groups)); + } + + this.selected.emit(groups[0]); + } + + ngOnInit(): void { + this.sub = this.service.groups.subscribe(groups => { + this.groups = groups; + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.html new file mode 100644 index 00000000..182e8bc5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.html @@ -0,0 +1,20 @@ +
+
+ +

Errors per application version

+
+
+ +
+
+
+
+

Business impact

+
+ +
+
+
+
+ There is no data to process. +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.spec.ts new file mode 100644 index 00000000..7cd4fea2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppInsightsDashboardComponent } from './dashboard.component'; + +describe('DashboardComponent', () => { + let component: AppInsightsDashboardComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AppInsightsDashboardComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AppInsightsDashboardComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.ts new file mode 100644 index 00000000..e6ebb496 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/dashboard/dashboard.component.ts @@ -0,0 +1,121 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { ApiClient } from "../../../utils/HttpClient"; +import { GetApplicationVersions, GetApplicationVersionsResult } from "../../../../server-api/Core/Applications"; +import { ChartService, IChartSeries, ILabelOptions } from "../../../services/chart.service"; + +interface IVersion { + version: string; + versionForRoutes: string; + reportCount: number; + reportSign?: "fa-arrow-up text-danger" | "fa-arrow-down text-success"; + reportPercentage?: number; + incidentCount: number; + incidentSign?: "fa-arrow-up text-danger" | "fa-arrow-down text-success"; + incidentPercentage?: number; +} + +@Component({ + selector: 'app-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.scss'] +}) +export class AppInsightsDashboardComponent implements OnInit, OnDestroy { + private sub: any; + applicationId: number = null; + versions: IVersion[] = []; + showNoData = true; + chartId = ""; + + constructor( + private readonly apiClient: ApiClient, + private chartService: ChartService, + private activatedRoute: ActivatedRoute) { + this.chartId = this.chartService.generateChartId(); + } + + ngOnInit(): void { + this.sub = this.activatedRoute.params.subscribe(params => { + this.applicationId = +params['applicationId']; + this.loadVersions(); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + private async loadVersions() { + var q2 = new GetApplicationVersions(); + q2.applicationId = this.applicationId; + var result = await this.apiClient.query(q2); + if (result.items.length === 0) { + return; + } + + this.showNoData = false; + this.versions = []; + for (var i = result.items.length - 1; i >= 0; i--) { + let dto = result.items[i]; + + var v: IVersion = { + version: dto.version, + versionForRoutes: dto.version.replace(/\./g, "_"), + reportCount: dto.reportCount, + incidentCount: dto.incidentCount + }; + + if (i < result.items.length - 1) { + var previousVersion = result.items[i + 1]; + + v.incidentPercentage = this.calculatePercentage(previousVersion.incidentCount, dto.incidentCount); + if (v.incidentPercentage < 0) { + v.incidentSign = "fa-arrow-down text-success"; + } else if (v.incidentPercentage > 0) { + if (v.incidentPercentage === Infinity) { + v.incidentPercentage = 0; + } + v.incidentSign = "fa-arrow-up text-danger"; + } + v.reportPercentage = this.calculatePercentage(previousVersion.reportCount, dto.reportCount); + if (v.reportPercentage < 0) { + v.reportSign = "fa-arrow-down text-success"; + } else if (v.reportPercentage > 0) { + if (v.reportPercentage === Infinity) { + v.reportPercentage = 0; + } + v.reportSign = "fa-arrow-up text-danger"; + } + } + + this.versions.push(v); + } + + var versions: string[] = []; + var volumes: number[] = []; + var series: IChartSeries[] = []; + this.versions.forEach(version => { + versions.push(version.version); + volumes.push(version.incidentCount); + }); + + series.push({ + name: 'Errors', + data: volumes + }); + + this.chartService.drawLineChart(this.chartId, { labels: versions }, series); + } + + private calculatePercentage(before: number, after: number): number { + //number of reports increased + if (before < after) { + let diff = after - before; + return Math.round(diff / before * 100); + } else { + let diff = before - after; + return Math.round(-(diff / before * 100)); + } + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.html new file mode 100644 index 00000000..29525a6c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.html @@ -0,0 +1 @@ +

details works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.spec.ts new file mode 100644 index 00000000..87ce5de0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppInsightsDetailsComponent } from './details.component'; + +describe('DetailsComponent', () => { + let component: AppInsightsDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AppInsightsDetailsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AppInsightsDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.ts new file mode 100644 index 00000000..830c34ac --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/applications/insights/details/details.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-details', + templateUrl: './details.component.html', + styleUrls: ['./details.component.scss'] +}) +export class AppInsightsDetailsComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/cloud/cloud.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/cloud/cloud.module.ts new file mode 100644 index 00000000..362aefed --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/cloud/cloud.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + + + +@NgModule({ + declarations: [], + imports: [ + CommonModule + ] +}) +export class CloudModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/c/c.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/c/c.module.ts new file mode 100644 index 00000000..ecadf75b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/c/c.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + + + +@NgModule({ + declarations: [], + imports: [ + CommonModule + ] +}) +export class CModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/commercial.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/commercial.module.ts new file mode 100644 index 00000000..e5c1892f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/commercial.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + + + +@NgModule({ + declarations: [], + imports: [ + CommonModule + ] +}) +export class CommercialModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/devops.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/devops.module.ts new file mode 100644 index 00000000..e0f81df5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/devops.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { ManageComponent } from './manage/manage.component'; + + + +@NgModule({ + declarations: [ + ManageComponent + ], + imports: [ + CommonModule + ] +}) +export class DevopsModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.html new file mode 100644 index 00000000..ad830f04 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.html @@ -0,0 +1,149 @@ +
+

Azure DevOps connection

+
+
+
+
+

Connection information

+
Attempting to connect, hang on...
+
Failed to connect to Azure Devops using the specified settings.
+

Information required so that Coderr can prioritize errors based on your own criteria.

+
+ + + Typically "https://dev.azure.com/yourOrganizationName/" +
+ +
+ + + Required to be able to connect. Work item access is enough when creating it in Azure DevOps. +
+ +
+ +
Configure settings above and click "Try connect"
+
+ +
+ +
+

Project information

+

Select which project and area that new issues should be added into.

+ +
+ + + Project that bugs should be added to. +
+ + +
+ + + Area that new bugs should be placed in +
+ +
+ + + How many items must be affected for an error to be deemed as critical/severe? +
+ +
+ +
+
+

These fields are used when synchronizing errors between Coderr and Azure DevOps. Make sure that they are correct, or the two-way synchronization wont work.

+ +
+ + + Which type of work item should be created for errors? +
+
+
+ + + Value used when assigning a work item. +
+
+ + + Value used when working with an error. +
+
+ + + State when a work item is closed directly (can be same as when solving in some process templates). +
+
+
+ + +
+ +
+
+

Should work items be created when Coderr escalates errors?

+
+ + +
+ When an error is escalated, create a bug in the backlog. +
+
+ + +
+ When an error is escalated, create a bug in the backlog. +
+
+
+
+ +
+
+ + + + +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.spec.ts new file mode 100644 index 00000000..d6bcdecc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ManageComponent } from './manage.component'; + +describe('ManageComponent', () => { + let component: ManageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ManageComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ManageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.ts new file mode 100644 index 00000000..1b8ed2dd --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/devops/manage/manage.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-manage', + templateUrl: './manage.component.html', + styleUrls: ['./manage.component.scss'] +}) +export class ManageComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/security/security.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/security/security.module.ts new file mode 100644 index 00000000..6ef14a42 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/commercial/security/security.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + + + +@NgModule({ + declarations: [], + imports: [ + CommonModule + ] +}) +export class SecurityModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.html new file mode 100644 index 00000000..c5f960b9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.html @@ -0,0 +1,30 @@ +
+

Generate demo errors

+
+

Are you using Node.js or .NET?

+ + + +
+
+

{{category.name}}

+
+
+ {{item.title}} +

+ {{item.description}} +

+
+
+
+ +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.scss new file mode 100644 index 00000000..667d2a8a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.scss @@ -0,0 +1,13 @@ +.onboarding-view hr { + color: #eaeaea; + background: #eaeaea; +} + +.onboarding-view .selected { + background: #59c1d5 !important; + color: #333 !important; +} + +.onboarding-view .selected strong, .onboarding-view .selected p { + color: #333 !important; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.spec.ts new file mode 100644 index 00000000..786359a8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DemoErrorsComponent } from './demo-errors.component'; + +describe('DemoErrorsComponent', () => { + let component: DemoErrorsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DemoErrorsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DemoErrorsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.ts new file mode 100644 index 00000000..93c3b175 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/demo-errors/demo-errors.component.ts @@ -0,0 +1,95 @@ +import { Component, OnInit } from '@angular/core'; +import * as Demo from "../../../server-api/Common/Demo"; +import { ApiClient } from "../../utils/HttpClient"; + +export interface IDemoItem { + description: string; + title: string; + id: string; + selected: boolean; +} + +interface IDemoCategory { + name: string; + items: IDemoItem[]; +} + +@Component({ + selector: 'app-demo-errors', + templateUrl: './demo-errors.component.html', + styleUrls: ['./demo-errors.component.scss'] +}) +export class DemoErrorsComponent implements OnInit { + framework: string = ''; + demoIncidents: IDemoCategory[] = []; + generated=false; + + constructor(private apiClient: ApiClient) { } + + ngOnInit(): void { + } + + + async showDemoOptions() { + var dto = new Demo.GetDemoIncidentOptions(); + var result = await this.apiClient.query(dto); + this.demoIncidents = []; + var category: IDemoCategory = null; + + result.items.forEach(dto => { + if (this.framework === 'nodejs') { + if (dto.category !== 'JavaScript' && dto.category !== "VueJS") { + return; + } + } + + if (category == null || dto.category !== category.name) { + category = { + name: dto.category, + items: [] + }; + + this.demoIncidents.push(category); + } + + var item = { + id: dto.id, + description: dto.description, + title: dto.title, + selected: false + }; + category.items.push(item); + }); + + } + + generateErrors() { + var itemsToGenerate: string[] = []; + var libs: string[] = []; + this.demoIncidents.forEach(category => { + category.items.forEach(dto => { + if (dto.selected) { + itemsToGenerate.push(dto.id); + if (!libs.find(x => x === category.name)) { + libs.push(category.name); + } + } + }); + }); + var cmd = new Demo.GenerateDemoIncidents(); + cmd.demoOptionIds = itemsToGenerate; + this.apiClient.command(cmd); + this.generated = true; + } + + selectFramework(value: string) { + this.framework = value; + this.showDemoOptions(); + } + + toggleMe(item: IDemoItem) { + item.selected = !item.selected; + } + + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.component.html new file mode 100644 index 00000000..0d2a89a1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.component.html @@ -0,0 +1,168 @@ +
+ +
+
+

Getting started

+

+ You are not a member of any applications in Coderr yet. Ask your administrator to get invited. +

+
+
+ +
+
+

Applications

+   +

My errors

+
+ +
+ New application
+
+ + +
(filtered on {{selectedGroup.name}}) +
+
+
+ +
+
+
No applications match your filter.
+
+
+

{{app.name}}

+
+
+ + + + + + + + + + + + + + + + + + + + + +
Errors + {{app.errors}} + (latest {{app.latestError|ago}}) +
Reports + {{app.reports}} + (latest {{app.latestReport|ago}}) +
Affected users{{app.ReportCount}}0
Users waiting{{app.followers}}
Bug reports{{app.bugReports}}
+
+
+ + + + +
+ +
+
+
+
+
+ +
+ +
+
+ + +

Filter

+
+

Once you've added a couple of applications, filter them to quickly find the application that you want to work with.

+
+
+ +

Application groups

+
+

Groups are used to categorize applications and manage permissions on a higher level.

+
+
+ +

My errors

+
+

Once you've started to work with some errors, you can find them here.

+
+
+ + + +
+

Add application

+
+ +
+
+
+ +
+

Select group to filter on

+
+
+
+

Application groups are used to logically group applications into smaller lists.

+
+
+
+
+
+ +
+
+
+ +
+

Create group

+
+

Application groups are used to logically group applications into smaller lists.

+ +
+
+
+ + + +

Short guides

+
+

When this icon is yellow, click on it to see a short summary of a feature in Coderr.

+

It's the best way to get started quickly and to learn about new features after a release.

+
+
+ + +

System administration

+
+

Here you can find security settings, api keys, group management and white lists (for front-ends).

+
+
+ + +

Support

+
+

We, the developers of this service, would love to help. You can also just message us to discuss about code quality.

+
+
+ + +

Account settings

+
+

Get notified ASAP when new errors are detected, or when they are escalated by Coderr.

+
+
+ + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.component.ts new file mode 100644 index 00000000..956fb5ee --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.component.ts @@ -0,0 +1,157 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ApplicationService } from "../applications/application.service"; +import { ApiClient } from "../utils/HttpClient"; +import { GetApplicationOverview, GetApplicationOverviewResult } from "../../server-api/Core/Applications"; +import { ModalService } from '../_controls/modal/modal.service'; +import { NavMenuService } from "../nav-menu/nav-menu.service"; +import { AuthorizeService } from "../../api-authorization/authorize.service"; +import { GuideService } from "../_controls/guide/guide.service"; + + +@Component({ + selector: 'app-home', + templateUrl: './home.component.html', +}) +export class HomeComponent implements OnInit, OnDestroy { + applications: ApplicationOverview[] = []; + private allApps: ApplicationOverview[] = []; + private appSub: any; + private filterText = ''; + selectedGroup?: IGroup; + noApps = false; + showGroupsStyle = "hidden"; + activePane = ''; + firstApplicationId = 0; + + constructor(private appService: ApplicationService, + private modalService: ModalService, + private apiClient: ApiClient, + private guideService: GuideService, + authService: AuthorizeService, + navMenuService: NavMenuService) { + navMenuService.updateNav([]); + + this.activePane = localStorage.getItem('homeActivePane') || 'applications'; + } + + filter(event) { + this.filterText = event.target.value.toLowerCase(); + this.executeFilter(); + } + + setPane(name: string) { + this.activePane = name; + localStorage.setItem('homeActivePane', name); + } + + toId(value: string) { + return 'app-' + value.replace(/\s+/g, '-'); + } + + ngOnInit(): void { + this.appSub = this.appService.applications.subscribe(apps => { + this.noApps = apps.length === 0; + this.allApps.length = 0; + + if (apps.length > 0) { + this.firstApplicationId = apps[0].id; + } + + apps.forEach(appDto => { + const app: ApplicationOverview = { + id: appDto.id, + groupIds: appDto.groupIds, + name: appDto.name, + errors: 0, + latestError: null, + latestReport: null, + partitions: [], + reports: 0, + followers: 0, + bugReports: 0 + }; + + this.allApps.push(app); + + var query = new GetApplicationOverview(); + query.applicationId = app.id; + query.includeChartData = false; + query.includePartitions = true; + query.numberOfDays = 30; + this.apiClient.query(query) + .then(result => { + app.reports = result.statSummary.reports; + app.errors = result.statSummary.incidents; + app.latestReport = result.statSummary.newestReportReceivedAtUtc; + app.latestError = result.statSummary.newestIncidentReceivedAtUtc; + app.followers = result.statSummary.followers; + app.bugReports = result.statSummary.userFeedback; + }); + }); + + this.applications = this.allApps; + + }); + + + } + + showGroups() { + this.modalService.open('groupFilter'); + } + + selectGroup(evt: IGroup) { + this.selectedGroup = evt; + this.modalService.close('groupFilter'); + this.executeFilter(); + } + createApplication() { + this.modalService.open('newAppModal'); + } + + closeCreateAppModal() { + this.modalService.close('newAppModal'); + } + + createGroup() { + this.modalService.open('createGroupModal'); + } + + onGroupCreated() { + this.modalService.close('createGroupModal'); + } + + ngOnDestroy(): void { + this.appSub.unsubscribe(); + } + + private executeFilter() { + var apps = this.allApps.filter(x => x.name.toLowerCase().indexOf(this.filterText) !== -1); + if (this.selectedGroup != null) { + apps = apps.filter(x => x.groupIds.includes(this.selectedGroup.id)); + } + this.applications = apps; + } +} + +interface PartitionOverview { + displayName: string; + count: number; +} +interface ApplicationOverview { + id: number; + groupIds: number[]; + name: string; + errors: number; + latestReport: string | null; + reports: number; + latestError: string | null; + followers: number; + bugReports: number; + partitions: PartitionOverview[]; +} + +interface IGroup { + id: number; + name: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.module.ts new file mode 100644 index 00000000..67866658 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/home.module.ts @@ -0,0 +1,41 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterModule, Routes } from '@angular/router'; +import { LatestErrorsComponent } from './latest-errors/latest-errors.component'; +import { HomeComponent } from './home.component'; +import { PipeModule } from "../../pipes/pipe.module"; +import { ControlsModule } from "../_controls/controls.module"; +import { ApplicationModule } from "../applications/application.module"; +import { ReactiveFormsModule } from '@angular/forms'; +import { IncidentsModule } from "../incidents/incidents.module"; +import { AuthorizeGuard } from "../../api-authorization/authorize.guard"; +import { AdminModule } from "../admin/admin.module"; +import { GroupModule } from "../admin/groups/group.module"; +import { DemoErrorsComponent } from './demo-errors/demo-errors.component'; +import { TipsComponent } from './tips/tips.component'; + +const incidentsRoutes: Routes = [ + { path: '', pathMatch: 'full', component: HomeComponent, canActivate: [AuthorizeGuard] }, + { path: 'demo-errors', component: DemoErrorsComponent }, + { path: 'tips', component: TipsComponent } +]; + + +@NgModule({ + declarations: [LatestErrorsComponent, HomeComponent, DemoErrorsComponent, TipsComponent], + imports: [ + ControlsModule, + AdminModule, + ApplicationModule, + CommonModule, + PipeModule, + GroupModule, + RouterModule.forChild(incidentsRoutes), + ReactiveFormsModule, + IncidentsModule + ], + exports: [ + + ] +}) +export class HomeModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.html new file mode 100644 index 00000000..255cce34 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.html @@ -0,0 +1 @@ +

latest-errors works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.spec.ts new file mode 100644 index 00000000..d986cd00 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LatestErrorsComponent } from './latest-errors.component'; + +describe('LatestErrorsComponent', () => { + let component: LatestErrorsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LatestErrorsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LatestErrorsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.ts new file mode 100644 index 00000000..2c2b399b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/latest-errors/latest-errors.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-latest-errors', + templateUrl: './latest-errors.component.html', + styleUrls: ['./latest-errors.component.scss'] +}) +export class LatestErrorsComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.html new file mode 100644 index 00000000..658d3472 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.html @@ -0,0 +1 @@ +

my-errors works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.spec.ts new file mode 100644 index 00000000..066e4de1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MyErrorsComponent } from './my-errors.component'; + +describe('MyErrorsComponent', () => { + let component: MyErrorsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ MyErrorsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(MyErrorsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.ts new file mode 100644 index 00000000..7e81e372 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/my-errors/my-errors.component.ts @@ -0,0 +1,20 @@ +import { Component, OnInit } from '@angular/core'; +import { IncidentsService } from "../../incidents/incidents.service"; + +@Component({ + selector: 'app-my-errors', + templateUrl: './my-errors.component.html', + styleUrls: ['./my-errors.component.scss'] +}) +export class MyErrorsComponent implements OnInit { + + + constructor(private readonly incidentService: IncidentsService) { + incidentService.myIncidents + } + + ngOnInit(): void { + + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.html new file mode 100644 index 00000000..114ae97c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.html @@ -0,0 +1,91 @@ +
+
+
+
+
+

Welcome to Coderr!

+
+
+
+
+
+

Report errors

+
+
+

+ The best way to get started is to read our getting started guide. +

+

+ Our nuget packages are used detect and report errors in your application. To get started you need to install and configure our of our packages. +

+

+ You can also read our wiki or visit our Guides and support section at our homepage. +

+
+
+
+
+

Before going to production

+
+

+ Coderr will by default throw exceptions and try to report errors directly to be able to tell if something is configured incorrectly. That's great + when you get started, but can have implications in production. To disable those features, do the following: +

+ +

Turn off Coderr's internal exceptions before going to production.

+
Err.Configuration.ThrowExceptions = false;
+ +

Let Coderr report errors in the background (to not slow down your application while it tries to report errors).

+
Err.Configuration.QueueReports = true;
+
+
+
+

Disable Coderr in Development environments

+
+
+

You typically have full control over errors happening in development environments.

+

Learn how to conditionally enable Coderr in different environments to reduce noise.

+
+ Learn more +
+
+ +
+

Demo errors

+
+
+

+ Coderr can generate some demo errors so that you can quickly can see what Coderr provides for information. +

+
+ Generate demo errors +
+
+ +
+

Application configuration (admin only)

+
+

+ You can monitor multiple applications in Coderr. To make it easier for you, we created an application named "DemoApp". +

+

+ You can change its name and create more applications by clicking on the Cog wheel () at top right. +

+
+
+ +
+

Invite co-workers (admin only)

+
+

To invite co-workers, use the Cog wheel () found in top right menu, select one of your applications and then use the "Security" tab.

+
+
+ + +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.spec.ts new file mode 100644 index 00000000..ea1944e6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TipsComponent } from './tips.component'; + +describe('TipsComponent', () => { + let component: TipsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ TipsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(TipsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.ts new file mode 100644 index 00000000..09e21081 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/home/tips/tips.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit } from '@angular/core'; +import { ApplicationService } from "../../applications/application.service"; + +@Component({ + selector: 'app-tips', + templateUrl: './tips.component.html', + styleUrls: ['./tips.component.scss'] +}) +export class TipsComponent implements OnInit { + firstApplicationId = 0; + + constructor(private appService: ApplicationService) { } + + ngOnInit(): void { + this.appService.list().then(apps => { + if (apps.length > 0) { + this.firstApplicationId = apps[0].id; + } + }); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/IncidentConverter.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/IncidentConverter.ts new file mode 100644 index 00000000..349ccd70 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/IncidentConverter.ts @@ -0,0 +1,225 @@ +import * as model from "./incident.model"; +import * as api from "../../server-api/Core/Incidents"; +import * as mine from "../../server-api/Common/Mine"; +import { ApiClient, HttpError } from "../utils/HttpClient"; +import { AuthorizeService } from "../../api-authorization/authorize.service"; + +export interface IIncidentMap { + item: model.Incident | null; + id: number; + refreshedAt: Date; + loadPromise: Promise; +} + +export interface ISearchQuery { + +} + +/** + * Loads incidents from the backend and caches them. + * Used by all services so that they can share incidents and enjoy updates. + */ +export class IncidentLoader { + constructor( + private readonly apiClient: ApiClient) { + + } + + + async getRecommendations(applicationId?: number): Promise { + var recommendations: model.IncidentRecommendation[] = []; + + + var query = new mine.ListMyIncidents(); + if (applicationId > 0) { + query.applicationId = applicationId; + } + + var result = await this.apiClient.query(query); + + // not supported in OSS. + if (!result) { + return null; + } + + result.suggestions.forEach(x => { + var item = new model.IncidentRecommendation(); + item.applicationId = x.applicationId; + item.applicationName = x.applicationName; + item.name = x.name; + item.id = x.id; + item.createdAtUtc = new Date(x.createdAtUtc + "Z"); + item.lastReportReceivedAtUtc = new Date(x.lastReportAtUtc + "Z"); + item.reportCount = x.reportCount; + item.exceptionTypeName = x.exceptionTypeName; + item.motivation = x.motivation.replace(/\r\n/g, "; "); + item.weight = x.weight; + recommendations.push(item); + }); + + return recommendations.sort((a, b) => a.weight - b.weight); + } + + async findForUser(accountId: number): Promise { + var foundIncidents: model.IncidentSummary[] = []; + + + var query = new mine.ListMyIncidents(); + var result = await this.apiClient.query(query); + if (!result) { + return []; + } + + result.items.forEach(x => { + var item = new model.IncidentSummary(); + item.applicationId = x.applicationId; + item.applicationName = x.applicationName; + item.name = x.name; + item.id = x.id; + item.assignedAtUtc = new Date(x.assignedAtUtc + "Z"); + item.createdAtUtc = new Date(x.createdAtUtc + "Z"); + item.lastReportReceivedAtUtc = new Date(x.lastReportAtUtc + "Z"); + item.reportCount = x.reportCount; + foundIncidents.push(item); + }); + + return foundIncidents; + } + + async search(text?: string, applicationId?: number, collectionName?: string, propertyName?: string, propertyValue?: string,): Promise { + var foundIncidents: model.IncidentSummary[] = []; + + var query = new api.FindIncidents(); + if (text) { + query.freeText = text; + } + if (applicationId) { + query.applicationIds = [applicationId]; + } + if (collectionName) { + query.contextCollectionName = collectionName; + } + if (propertyName) { + query.contextCollectionPropertyName = propertyName; + } + if (propertyValue) { + query.contextCollectionPropertyValue = propertyValue; + } + var result = await this.apiClient.query(query); + result.items.forEach(x => { + var item = new model.IncidentSummary(); + item.applicationId = x.applicationId; + item.applicationName = x.applicationName; + item.name = x.name; + item.id = x.id; + item.assignedAtUtc = x.assignedAtUtc; + item.createdAtUtc = x.createdAtUtc; + item.lastReportReceivedAtUtc = x.lastReportReceivedAtUtc; + item.assignedAtUtc = x.assignedAtUtc; + foundIncidents.push(item); + }); + + return foundIncidents; + } + + public async refreshIncident(incident: model.Incident, callback?: () => void): Promise { + const query = new api.GetIncident(); + query.incidentId = incident.id; + const result = await this.apiClient.query(query); + this.convertIncidentResult(result, incident); + if (callback) { + callback(); + } + + return incident; + } + + private convertIncidentResult(result: api.GetIncidentResult, incident: model.Incident) { + incident.applicationId = result.applicationId; + incident.assignedAtUtc = result.assignedAtUtc; + incident.assignedTo = result.assignedTo; + incident.assignedToId = result.assignedToId; + incident.contextCollections = result.contextCollections; + incident.createdAtUtc = result.createdAtUtc; + incident.description = result.description; + incident.facts = this.convertFacts(result.facts); + incident.state = result.incidentState; + incident.fullName = result.fullName; + incident.highlightedContextData = this.convertData(result.highlightedContextData); + incident.isIgnored = result.isIgnored; + incident.isReOpened = result.isReOpened; + incident.isSolved = result.isSolved; + incident.lastReportReceivedAtUtc = result.lastReportReceivedAtUtc; + incident.reOpenedAtUtc = result.reOpenedAtUtc; + incident.reportCount = result.reportCount; + incident.solution = result.solution; + incident.solvedAtUtc = result.solvedAtUtc; + incident.stackTrace = result.stackTrace; + incident.suggestedSolutions = this.convertSolutions(result.suggestedSolutions); + incident.tags = result.tags; + incident.updatedAtUtc = result.updatedAtUtc; + + var stats = new model.IncidentMonthStats(); + stats.days = []; + stats.values = []; + result.dayStatistics.map(x => { + stats.days.push(x.date); + stats.values.push(x.count); + }); + incident.monthReports = stats; + } + + private convertFacts(highlightedContextData: api.QuickFact[]): model.IQuickFact[] { + var output: model.IQuickFact[] = []; + highlightedContextData.forEach(x => { + output.push({ + description: x.description, + title: x.title, + value: x.value, + url: x.url + }); + }); + return output; + } + + private convertData(highlightedContextData: api.HighlightedContextData[]): model.IHighlightedData[] { + var output: model.IHighlightedData[] = []; + highlightedContextData.forEach(x => { + output.push({ + name: x.name, + value: x.value, + description: x.description, + url: x.url + }); + }); + return output; + } + + private convertSolutions(highlightedContextData: api.SuggestedIncidentSolution[]): model.ISuggestedSolution[] { + var output: model.ISuggestedSolution[] = []; + highlightedContextData.forEach(x => { + output.push({ + reason: x.reason, + suggestedSolution: x.suggestedSolution, + }); + }); + return output; + } + + private convertState(state: number): string { + switch (state) { + case model.IncidentState.New: + return "New"; + case model.IncidentState.Active: + return "Active"; + case model.IncidentState.Ignored: + return "Ignored"; + case model.IncidentState.Closed: + return "Closed"; + case model.IncidentState.ReOpened: + return "ReOpened"; + default: + return "Unknown"; + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.html new file mode 100644 index 00000000..35a7e909 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.html @@ -0,0 +1,5 @@ +
+
+
+ The impact of this error is not tracked. Read the prioritization guide to configure. +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.spec.ts new file mode 100644 index 00000000..2c81528a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { InsightChartComponent } from './insightchart.component'; + +describe('InsightChartComponent', () => { + let component: InsightChartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [InsightChartComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(InsightChartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.ts new file mode 100644 index 00000000..887d161d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/insights/insightchart.component.ts @@ -0,0 +1,74 @@ +import { Component, OnInit, Input } from '@angular/core'; +import * as api from "../../../../server-api/Common/Partitions"; +import { ApiClient } from "../../../utils/HttpClient"; +import { ChartService, IChartSeries } from "../../../services/chart.service"; + +@Component({ + selector: 'incident-insight-chart', + templateUrl: './insightchart.component.html', + styleUrls: ['./insightchart.component.scss'] +}) +export class InsightChartComponent implements OnInit { + private _applicationId: number; + private _incidentId: number; + chartId = ''; + showGotNone = false; + + constructor( + private apiClient: ApiClient, + private chartService: ChartService + ) { + this.chartId = this.chartService.generateChartId(); + } + + @Input() + get applicationId(): number { return this._applicationId; } + set applicationId(applicationId: number) { + this._applicationId = applicationId; + this.loadStats(); + } + + @Input() + get incidentId(): number { return this._incidentId; } + set incidentId(incidentId: number) { + this._incidentId = incidentId; + this.loadStats(); + } + + ngOnInit(): void { + } + + private async loadStats() { + const gotAll = this.applicationId > 0 && this._incidentId > 0; + if (!gotAll) { + return; + } + + const query = new api.GetPartitionInsights(); + query.applicationIds = [this.applicationId]; + query.incidentId = this._incidentId; + const result = await this.apiClient.query(query); + + // null = not supported. + if (!result || result.applications.length === 0) { + this.showGotNone = true; + return; + } + + if (result.applications[0].indicators.length === 0) { + this.showGotNone = true; + return; + } + + var series: IChartSeries[] = []; + result.applications[0].indicators.forEach(indicator => { + series.push({ + name: indicator.displayName, + data: indicator.values + }); + }); + + const legend = this.chartService.generateLabelsFromStringDate(result.applications[0].indicators[0].dates); + this.chartService.drawLineChart(this.chartId, { labels: legend }, series); + }; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.html new file mode 100644 index 00000000..bb8ba56c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.spec.ts new file mode 100644 index 00000000..381ddc7d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SummaryChartComponent as SummarychartComponent } from './summary.component'; + +describe('SummaryChartComponent', () => { + let component: SummarychartComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SummarychartComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SummarychartComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.ts new file mode 100644 index 00000000..5967b3a7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/charts/summary/summary.component.ts @@ -0,0 +1,61 @@ +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import * as api from "../../../../server-api/Core/Applications"; +import { ApiClient } from "../../../utils/HttpClient"; +import { ChartService, IChartSeries } from "../../../services/chart.service"; + +@Component({ + selector: 'app-summarychart', + templateUrl: './summary.component.html', + styleUrls: ['./summary.component.scss'] +}) +export class SummaryChartComponent implements OnInit, OnDestroy { + private _applicationId: number; + private static counter = 1; + private timer: any; + chartId: string = "summaryChart"; + + constructor( + private apiClient: ApiClient, + private chartService: ChartService + ) { + this.chartId = this.chartService.generateChartId(); + this.timer = setInterval(this.onRefreshStats, 5000); + } + + @Input() + get applicationId(): number { return this._applicationId; } + set applicationId(applicationId: number) { + this._applicationId = applicationId; + this.loadStats(); + } + + ngOnInit(): void { + } + + ngOnDestroy(): void { + clearTimeout(this.timer); + } + + private onRefreshStats() { + this.loadStats(); + } + + private async loadStats() { + var query = new api.GetApplicationOverview(); + query.applicationId = this.applicationId; + var result = await this.apiClient.query(query); + + var series: IChartSeries[] = []; + series.push({ + name: 'Distinct new errors', + data: result.incidents + }); + series.push({ + name: 'Received error reports', + data: result.errorReports + }); + + this.chartService.drawLineChart(this.chartId, { labels: result.timeAxisLabels, tickCount: 15 }, series) + } +} + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.html new file mode 100644 index 00000000..a0ebe473 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.html @@ -0,0 +1,53 @@ +
+
+
+

Bug reports (sent from users)

+
+
+
+
+

+ There is no bug reports from users. Use Err.LeaveFeedback() to attach bug reports to errors, or activate the built in error forms. +

+
+
+
+
+

+ {{report.message}} +

+
+ + written {{report.writtenAtUtc|ago}} + + by {{report.email}} + + +
+
+
+
+ +
+
+

Send status updates

+
+

There are currently {{emails.length}} user(s) waiting to hear about this bug. Write them a small status update.

+
+
+ + +
+
+ + +
+
+ +
+ +
+
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.scss new file mode 100644 index 00000000..7fddeb6b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.scss @@ -0,0 +1,3 @@ +label{ + font-weight: bold; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.spec.ts new file mode 100644 index 00000000..24fc1d6a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BugReportsComponent } from './bugreports.component'; + +describe('BugreportsComponent', () => { + let component: BugReportsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [BugReportsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(BugReportsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.ts new file mode 100644 index 00000000..89eda9a5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/bugreports/bugreports.component.ts @@ -0,0 +1,89 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { ActivatedRoute } from "@angular/router"; +import { FormBuilder } from '@angular/forms'; +import * as api from "../../../../server-api/Web/Feedback"; +import { NotifySubscribers } from "../../../../server-api/Core/Incidents"; +import { ApiClient } from "../../../utils/HttpClient"; +import { ToastrService } from 'ngx-toastr'; + +export interface IBugReport { + message: string; + email: string; + writtenAtUtc: Date; +} + +@Component({ + selector: 'incident-bugreports', + templateUrl: './bugreports.component.html', + styleUrls: ['./bugreports.component.scss'] +}) +export class BugReportsComponent implements OnInit { + private _incidentId: number; + private sub: any; + emails: string[] = []; + feedback: IBugReport[] = []; + form = this.formBuilder.group({ + subject: '', + body: '', + }); + + constructor( + private formBuilder: FormBuilder, + private apiClient: ApiClient, + private activeRoute: ActivatedRoute, + private toastrService: ToastrService + ) { + } + + ngOnInit(): void { + this.sub = this.activeRoute.parent.params.subscribe(params => { + this._incidentId = +params['incidentId']; + if (this._incidentId > 0) { + this.loadFeedback(); + } + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + + @Input() + get incidentId(): number { return this._incidentId; } + set incidentId(incidentId: number) { + this._incidentId = incidentId; + this.loadFeedback(); + } + + sendReport() { + var cmd = new NotifySubscribers(); + cmd.incidentId = this.incidentId; + cmd.body = this.form.value.body; + cmd.title = this.form.value.subject; + this.apiClient.command(cmd) + .then(x => { + this.toastrService.success('Message have been sent.'); + + }); + } + + private async loadFeedback() { + var query = new api.GetIncidentFeedback(); + query.incidentId = this._incidentId; + var result = await this.apiClient.query(query); + this.feedback = result.items.map(this.convertItem); + this.emails = result.emails; + } + + private convertItem(dto: api.GetIncidentFeedbackResultItem): IBugReport { + var item: IBugReport = { + email: dto.emailAddress, + message: dto.message, + writtenAtUtc: new Date(dto.writtenAtUtc + "Z") + }; + + return item; + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.html new file mode 100644 index 00000000..6b890b21 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.html @@ -0,0 +1,31 @@ +
+ +
+ + +
+
+ + +
+
+ {{errorMessage}} + Learn more about this feature
+ + +
+
+ + +
+

Users are tracking this error

+
+

There are {{numberOfUsers}} user(s) waiting to get notified when this error has been fixed.

+

Do you want to notify them?

+ + +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.spec.ts new file mode 100644 index 00000000..7711189c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { CloseComponent } from './close.component'; + +describe('CloseComponent', () => { + let component: CloseComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ CloseComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CloseComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.ts new file mode 100644 index 00000000..d6110c0e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/close/close.component.ts @@ -0,0 +1,70 @@ +import { Component, OnInit, Output, EventEmitter, Input } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { IncidentsService } from "../../incidents.service"; +import { ModalService } from "../../../_controls/modal/modal.service"; + +@Component({ + selector: 'incident-close', + templateUrl: './close.component.html', + styleUrls: ['./close.component.scss'] +}) +export class CloseComponent implements OnInit { + form = this.formBuilder.group({ + reason: '', + version: '', + }); + _incidentId = 0; + numberOfUsers = 0; + errorMessage = ''; + returnUrl = ''; + disabled = false; + + @Output() closed = new EventEmitter(); + + constructor( + private formBuilder: FormBuilder, + private incidentService: IncidentsService, + private modalService: ModalService) { + + + } + + @Input() + get incidentId(): number { return this._incidentId; } + set incidentId(incidentId: number) { + this._incidentId = incidentId; + } + + ngOnInit() { + } + + ngOnDestroy() { + } + + cancel() { + this.closed.emit({ success: false, message: "Canceled" }); + } + + onSubmit(): void { + this.incidentService.close(this._incidentId, this.form.value.version, this.form.value.reason) + .then(numberOfSubscribers => { + this.numberOfUsers = numberOfSubscribers; + if (numberOfSubscribers > 0) { + this.modalService.open("askSubscriberModal"); + } + }); + + this.disabled = true; + this.closed.emit({ success: true, version: this.form.value.version }); + } + + notifyUsers() { + this.closeNotifyDialog(); + + } + + closeNotifyDialog() { + this.modalService.close("askSubscriberModal"); + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.html new file mode 100644 index 00000000..508d443b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.html @@ -0,0 +1,5 @@ + + +
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.scss new file mode 100644 index 00000000..3f2719b5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.scss @@ -0,0 +1,85 @@ +@import "../../../styles/_partials/coderr-variables.scss"; +@import "../../../styles/_partials/_mixins.scss"; + +.incident-view { + max-width: 100%; + display: block; + margin: 0; + + h3 { + color: white; + @include text-shadow-1; + } + + .fill { + background: white; + padding: 10px; + border-radius: 2px; + @include box-shadow-1; + + th { + text-align: left; + } + + &.facts th { + text-align: right; + font-weight: bold; + } + } + + pre, .hide-overflow { + background-color: transparent; + overflow: hidden; + word-wrap: normal; + white-space: pre; + position: relative; + + > code { + background-color: transparent; + } + } + + .hide-overflow { + overflow: hidden; + word-wrap: normal; + white-space: pre; + position: relative; + } + + pre:hover, .hide-overflow:hover { + overflow: auto; + } + + select { + padding: 4px; + background-color: $light; + border: 1px solid darken($light, 10px); + border-radius: 5px; + } + + + + table { + table-layout: fixed; /* This enforces the "col" widths. */ + width: 100%; + + th { + vertical-align: top; + width: 200px; + overflow: hidden; + } + + th hover { + overflow: auto; + } + + td { + overflow: hidden; + white-space: pre; + + &:hover { + overflow: auto; + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.spec.ts new file mode 100644 index 00000000..3505e504 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DetailsComponent } from './details.component'; + +describe('DetailsComponent', () => { + let component: DetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ DetailsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(DetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.ts new file mode 100644 index 00000000..1191ab63 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/details.component.ts @@ -0,0 +1,62 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { IncidentsService } from "../incidents.service"; +import { Incident } from "../incident.model"; +import { AccountService, User } from "../../accounts/account.service"; +import { NavMenuService } from "../../nav-menu/nav-menu.service"; +import { ApplicationService } from "../../applications/application.service"; + +@Component({ + selector: 'incident-details', + templateUrl: './details.component.html', + styleUrls: ['./details.component.scss'] +}) +export class DetailsComponent implements OnInit, OnDestroy { + incident = new Incident(); + incidentId: number; + applicationId: number; + users: User[] = []; + + + private sub: any; + + constructor( + private incidentService: IncidentsService, + private route: ActivatedRoute, + private applicationService: ApplicationService, + private accountService: AccountService, + private menuService: NavMenuService, + ) { + this.incident.description = "Loading"; + this.incident.id = 0; + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.incidentId = +params['incidentId']; + this.loadEverything(); + }); + + this.accountService.getAllButMe() + .then(users => { + this.users = users; + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + + private async loadEverything() { + this.incident = await this.incidentService.get(this.incidentId); + this.applicationId = this.incident.applicationId; + const app = await this.applicationService.get(this.incident.applicationId); + this.menuService.updateNav([ + { title: app.name, route: ['application', app.id] }, + { title: 'Errors', route: ['application', app.id, 'errors'] } + ] + ); + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.html new file mode 100644 index 00000000..da78b3d4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.html @@ -0,0 +1,83 @@ +
+
+ +
+

Stack trace

+
+
{{incident.stackTrace}}
+
+ +

+ Report browser + (current: {{ currentReport.createdAtUtc|ago:'never':'full' }}) +

+
+
+ + + + + + + + +
+
+ {{prop.key}} +
+
+ {{prop.value}} +
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +

Product impact

+
+ + + + + +
{{fact.title}}
+
+

Reports

+
+
+
+ +

Affect

+
+ +
+ +
+
+ +
+ + +
+

Choose error report

+
+ +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.scss new file mode 100644 index 00000000..c402922b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.scss @@ -0,0 +1,15 @@ +@import "../details.component.scss"; + +.striped { + td { + padding: 5px; + } + + th { + padding: 5px; + } + + tr:nth-child(even) { + background-color: lighten($blue, 35%); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.spec.ts new file mode 100644 index 00000000..2c5a1726 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HomeComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.ts new file mode 100644 index 00000000..e56126bd --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/home/home.component.ts @@ -0,0 +1,153 @@ +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import { Incident } from "../../incident.model"; +import { IncidentsService } from "../../incidents.service"; +import { ActivatedRoute } from "@angular/router"; +import { ReportService, ReportSummary, Report, ReportCollection } from "../../report.service"; +import { ChartService } from "../../../services/chart.service"; +import { ModalService } from "../../../_controls/modal/modal.service"; + +@Component({ + selector: 'incident-details-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent implements OnInit, OnDestroy { + private sub: any; + incidentId: number; + applicationId: number; + incident = new Incident(); + reports: ReportSummary[] = []; + currentReport: Report; + currentCollection: ReportCollection; + currentCollectionName: string; + canShowNext = true; + canShowPrevious = true; + reportIndex = 0; + + constructor( + private incidentService: IncidentsService, + private route: ActivatedRoute, + private chartService: ChartService, + private reportService: ReportService, + private modalService: ModalService + ) { + + } + + ngOnInit(): void { + this.sub = this.route.parent.params.subscribe(params => { + this.incidentId = +params['incidentId']; + this.loadEverything(); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + private async loadEverything() { + this.incident = await this.incidentService.get(this.incidentId); + this.applicationId = this.incident.applicationId; + const labels = this.chartService.generateLabelsFromStringDate(this.incident.monthReports.days, { month: 'short', day: 'numeric' }); + this.chartService.drawLineChart('dayReports', + { + labels: labels + }, + [ + { + name: 'Received reports', + data: this.incident.monthReports.values + } + ]); + + this.reports = await this.reportService.getReportList(this.incidentId); + if (this.reports.length > 0) { + this.selectReport(this.reports[0].id); + } + + } + + + async selectReport(reportId: number): Promise { + const report = await this.reportService.getReport(reportId); + this.currentReport = report; + if (report.contextCollections.length > 0) { + + // Resume with the one that we used previously. + let collection = report.contextCollections.filter(x => x.name === this.currentCollectionName); + if (collection.length > 0) { + this.selectCollection(collection[0].name); + return; + } + + // Specified by the user + collection = report.contextCollections.filter(x => x.name === "HighlightCollections" || x.name === "HighlightCollection"); + if (collection.length > 0) { + this.selectCollection(collection[0].name); + return; + } + + // When the user supplies data, show it. + collection = report.contextCollections.filter(x => x.name === 'ContextData'); + if (collection.length > 0) { + this.selectCollection(collection[0].name); + return; + } + + collection = report.contextCollections.filter(x => x.name === 'ExceptionProperties'); + if (collection.length > 0) { + this.selectCollection(collection[0].name); + return; + } + + this.selectCollection(report.contextCollections[0].name); + } + + + this.canShowNext = true; + this.canShowPrevious = true; + if (this.reportIndex === 0) { + this.canShowPrevious = false; + } + if (this.reportIndex === this.reports.length - 1) { + this.canShowNext = false; + } + } + + + showNextReport() { + if (this.reportIndex < this.reports.length - 1) { + this.reportIndex++; + this.selectReport(this.reports[this.reportIndex].id); + } + + } + + showPreviousReport() { + if (this.reportIndex > 0) { + this.reportIndex--; + this.selectReport(this.reports[this.reportIndex].id); + } + + } + + + showReportSelector() { + this.modalService.open("chooseReportModal"); + } + selectSpecificReport(report: ReportSummary) { + this.selectReport(report.id); + this.modalService.close("chooseReportModal"); + } + + selectCollection(name: string) { + const collection = this.currentReport.contextCollections.filter(x => x.name === name); + if (collection.length > 0) { + this.currentCollectionName = name; + this.currentCollection = collection[0]; + } else { + this.currentCollectionName = null; + this.currentCollection = null; + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.html new file mode 100644 index 00000000..ff2ca020 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.html @@ -0,0 +1,36 @@ +
+

Business impact

+
+
+
+
+

Select indicator

+ {{item.name}} +
+
+ +
+ +
+

Submitted values

+ + + + + + + + + + + + + +
ValueLast received
{{item.value}}{{item.receivedAtUtc|ago}}
+
+ No items where found for the given indicator. +
+ +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.scss new file mode 100644 index 00000000..16ed2404 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.scss @@ -0,0 +1,3 @@ +.form-group a { + display: block; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.spec.ts new file mode 100644 index 00000000..f281efb1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ImpactComponent } from './impact.component'; + +describe('ImpactComponent', () => { + let component: ImpactComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ImpactComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ImpactComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.ts new file mode 100644 index 00000000..f26fe5c1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/impact/impact.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from "@angular/router"; +import * as dto from "../../../../server-api/Common/Partitions" +import { IncidentsService } from "../../incidents.service"; +import { ApiClient } from "../../../utils/HttpClient"; + +@Component({ + selector: 'app-impact', + templateUrl: './impact.component.html', + styleUrls: ['./impact.component.scss'] +}) +export class ImpactComponent implements OnInit, OnDestroy { + solution = ""; + partitions: dto.GetPartitionsResultItem[] = []; + values: dto.GetPartitionValuesResultItem[] = []; + incidentId: number = 0; + private sub: any; + + constructor( + private readonly apiClient: ApiClient, + private activeRoute: ActivatedRoute, + private readonly incidentService: IncidentsService) { + + } + + ngOnInit(): void { + this.sub = this.activeRoute.parent.params.subscribe(params => { + this.incidentId = +params['incidentId']; + this.incidentService.get(this.incidentId) + .then(incident => { + var query = new dto.GetPartitions(); + query.applicationId = incident.applicationId; + this.apiClient.query(query) + .then(x => { + this.partitions = x.items; + console.log('partitions', x); + }); + }); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + + selectItem(item: dto.GetPartitionsResultItem) { + var query = new dto.GetPartitionValues(); + query.incidentId = this.incidentId; + query.partitionId = item.id; + this.apiClient.query(query) + .then(result => { + this.values = result.items; + }); + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.html new file mode 100644 index 00000000..434b3850 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.html @@ -0,0 +1,39 @@ +
+
+

+ Log entries +

+
+ No entries has been attached. +

Coderr will automatically attached the 100 most recent log entries (before the exception occurred) if you are using one of our log library integrations.

+
+
+
+
+
+

Log entries

+
+ +
+
+ + + + + + + + + + + + + +
WhenEntry
{{entry.timeStampUtc|isoDate}} +
+
+
+
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.scss new file mode 100644 index 00000000..a9bce186 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.scss @@ -0,0 +1,33 @@ +table td { + font-weight: normal +} + +.log-1 { + color: #999999; +} + +.log-2 { + color: green; +} + +.log-3 { + color: #9932cc; +} + +.log-4 { + color: #9932cc; +} + +.log-5 { + color: #9932cc; +} + +.log-6 { + color: #9932cc; +} + + +.exception { + border-top: 1px solid #ccc; + color: #333; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.spec.ts new file mode 100644 index 00000000..12a4e107 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LogsComponent } from './logs.component'; + +describe('LogsComponent', () => { + let component: LogsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ LogsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(LogsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.ts new file mode 100644 index 00000000..f387d0f4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/logs/logs.component.ts @@ -0,0 +1,73 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ApiClient } from "../../../utils/HttpClient"; +import { ActivatedRoute } from "@angular/router"; +import * as api from "../../../../server-api/Common/Logs"; + +interface ILogEntry { + message: string; + logLevel: number; + exception: string; + timeStampUtc: Date; +} + +@Component({ + selector: 'app-logs', + templateUrl: './logs.component.html', + styleUrls: ['./logs.component.scss'] +}) +export class LogsComponent implements OnInit, OnDestroy { + private sub: any; + incidentId: number = 0; + entries: ILogEntry[] = []; + allEntries: ILogEntry[] = []; + filterText = ""; + + constructor( + private readonly apiClient: ApiClient, + private route: ActivatedRoute) { + + } + + ngOnInit(): void { + this.sub = this.route.parent.params.subscribe(params => { + this.incidentId = +params['incidentId']; + var q = new api.GetLogs(); + q.incidentId = this.incidentId; + this.apiClient.query(q) + .then(result => { + result.entries.forEach(x => { + if (x.message) { + this.allEntries.push({ + message: this.escapeHtml(x.message).replace(/\r\n/, "
\r\n"), + exception: this.escapeHtml(x.exception).replace(/\r\n/, "
\r\n"), + timeStampUtc: x.timeStampUtc, + logLevel: x.level + }); + + } + }); + this.entries = this.allEntries; + }); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + filterEntries($event: KeyboardEvent) { + var re = new RegExp(this.filterText, 'i'); + this.entries = this.allEntries.filter(x => re.test(x.message) || re.test(x.exception)); + } + + private escapeHtml(unsafe: string): string { + if (!unsafe) { + return ""; + } + return unsafe + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.html new file mode 100644 index 00000000..b6f65942 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.html @@ -0,0 +1,54 @@ + + + + +
+

Close error

+
+ + +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.scss new file mode 100644 index 00000000..5afcc4f4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.scss @@ -0,0 +1,100 @@ +@import "../../../../styles/_partials/coderr-variables.scss"; +@import "../../../../styles/_partials/_mixins.scss"; + +.submenu { + color: #ddd; + background: $nav-sub-bg; + padding-top: 10px; + padding-left: 15px; + padding-right: 15px; + + .groups { + margin-top: 10px; + margin-bottom: 10px; + + a { + background-color: $nav-sub-tab; + padding: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + } + } + + a { + color: $nav-text; + text-decoration: none; + } + + .application-list { + display: flex; + flex-direction: column; + } + + .application-list div { + padding: 5px; + } + + .state { + margin-top: 10px; + padding-bottom: 10px; + + dl { + display: inline; + + dt { + display: inline; + color: #999; + margin-right: 5px; + } + + dd { + display: inline; + margin-left: 0; + margin-right: 10px; + + select { + border-radius: 3px; + padding: 2px; + background-color: rgba(0, 0, 0, 0.3); + border-color: rgba(255, 255, 255, 0.1); + color: #ddd; + } + } + } + + .tags { + display: inline; + + span { + border-radius: 2px; + padding: 2px; + font-size: 0.8em; + background-color: darken($blue, 40%); + margin-right: 5px; + } + } + } + + .actions { + margin-left: auto; + margin-bottom: 3px; + margin-top: auto; + + a { + color: darken($blue, 50%); + padding: 3px 5px; + border-top: 1px solid darken($blue, 10%); + border-left: 1px solid darken($blue, 10%); + border-right: 1px solid darken($blue, 10%); + background-color: $blue; + + &.active { + color: lighten($blue, 50%); + } + + &:hover { + color: $red; + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.spec.ts new file mode 100644 index 00000000..f8ccd6f4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NavbarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.ts new file mode 100644 index 00000000..e02e3234 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/navbar/navbar.component.ts @@ -0,0 +1,83 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { IncidentsService } from "../../incidents.service"; +import { IncidentState, Incident, states, IState } from "../../incident.model"; +import { ModalService } from "../../../_controls/modal/modal.service"; +import { ToastrService } from "ngx-toastr"; +import { AccountService, User } from "../../../accounts/account.service"; + +@Component({ + selector: 'incident-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'] +}) +export class NavbarComponent implements OnInit { + incident = new Incident(); + states = states; + myIncidentId: number; + users: User[] = []; + + constructor( + private incidentService: IncidentsService, + private modalService: ModalService, + private toastrService: ToastrService, + private accountService: AccountService + ) { + } + + ngOnInit(): void { + } + + @Input() + get incidentId(): number { return this.myIncidentId; } + set incidentId(incidentId: number) { + this.myIncidentId = incidentId; + if (this.myIncidentId > 0) { + this.loadEverything(); + } + } + + + assignIncident(accountId: number) { + this.incidentService.assign(this.incidentId, +accountId); + this.incident.state = IncidentState.Active; + this.toastrService.success("Error has been assigned."); + } + + changeState(stateId: number) { + var state = this.states[stateId]; + + switch (state.id) { + case IncidentState.Active: + this.assignIncident(-1); + break; + + case IncidentState.Closed: + this.close(); + break; + + case IncidentState.Ignored: + this.incidentService.ignore(this.incidentId); + this.toastrService.success("Future reports of this error will be ignored."); + break; + + default: + this.toastrService.warning(`Changing state to ${state.name} is invalid when state is ${this.incident.state}.`); + } + + } + + close() { + this.modalService.open("closeIncidentModal"); + + } + + closeCloseModal(evt: any) { + this.modalService.close("closeIncidentModal"); + + } + + private async loadEverything() { + this.incident = await this.incidentService.get(this.myIncidentId); + this.users = await this.accountService.getAll(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.html new file mode 100644 index 00000000..f8c1e6aa --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.html @@ -0,0 +1,35 @@ +
+
+
+

+ Origins +

+
+
+ You must create an account at https://ipstack.com/. Then add a new row in the "Settings" table in your database: + + + + + + + + + + + + + +
SectionOrigins
NameApiKey
ValueYour api key
+
+

Displays where we received the error reports from.

+ +
+
+ +
+
+
+
+ +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.spec.ts new file mode 100644 index 00000000..4435e553 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { OriginsComponent } from './origins.component'; + +describe('OriginsComponent', () => { + let component: OriginsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ OriginsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(OriginsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.ts new file mode 100644 index 00000000..9cf1677c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/origins/origins.component.ts @@ -0,0 +1,100 @@ +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; +import { ApiClient } from "../../../utils/HttpClient"; +import { ActivatedRoute } from "@angular/router"; +import { IncidentsService } from "../../incidents.service"; +import { GetOriginsForIncident, GetOriginsForIncidentResult } from "../../../../server-api/Modules/ErrorOrigins"; +declare var google: any; + +@Component({ + selector: 'app-origins', + templateUrl: './origins.component.html', + styleUrls: ['./origins.component.scss'] +}) +export class OriginsComponent implements OnInit, OnDestroy { + incidentId: number = 0; + gotItems = true; + private sub: any; + @ViewChild('mapScript') mapScript: ElementRef; + + constructor( + private readonly apiClient: ApiClient, + private activeRoute: ActivatedRoute, + private readonly incidentService: IncidentsService) { + + } + + ngOnInit(): void { + this.sub = this.activeRoute.parent.params.subscribe(params => { + this.incidentId = +params['incidentId']; + + }); + } + + ngAfterViewInit() { + this.loadOrigins(this.incidentId); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + private async loadOrigins(incidentId: number): Promise { + const js = document.createElement("script"); + js.type = "text/javascript"; + js.src = + "https://maps.googleapis.com/maps/api/js?key=AIzaSyBleXqcxCLRwuhcXk-3904HaJt9Vd1-CZc&libraries=visualization"; + + var el = this.mapScript.nativeElement; + el.appendChild(js); + + js.onload = () => { + const mapDiv = document.getElementById("map"); + var map = new google.maps.Map(mapDiv, + { + zoom: 3, + center: { lat: 37.775, lng: -122.434 } + }); + google.maps.event.addDomListener(window, + "resize", + () => { + const center = map.getCenter(); + google.maps.event.trigger(map, "resize"); + map.setCenter(center); + }); + + + const query = new GetOriginsForIncident(); + query.incidentId = incidentId; + this.apiClient.query(query) + .then(response => { + //disabled in commercial + //this.gotItems = response.Items.length > 0; + + if (response.items.length < 50) { + response.items.forEach(item => { + var point = new google.maps.LatLng(item.latitude, item.longitude); + var marker = new google.maps.Marker({ + position: point, + map: map, + title: item.numberOfErrorReports + " error report(s)" + }); + }); + } else { + var points: any[] = []; + response.items.forEach(item => { + var point = new google.maps.LatLng(item.latitude, item.longitude); + points.push({ location: point, weight: item.numberOfErrorReports }); + }); + + var heatmap = new google.maps.visualization.HeatmapLayer({ + data: points, + map: map, + dissipating: true, + radius: 15 + }); + } + + }); + }; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.html new file mode 100644 index 00000000..6a8ebc2b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.html @@ -0,0 +1 @@ +

reports works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.spec.ts new file mode 100644 index 00000000..ae4a7687 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ReportsComponent } from './reports.component'; + +describe('ReportsComponent', () => { + let component: ReportsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ReportsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ReportsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.ts new file mode 100644 index 00000000..bb276a46 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/details/reports/reports.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-reports', + templateUrl: './reports.component.html', + styleUrls: ['./reports.component.scss'] +}) +export class ReportsComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.html new file mode 100644 index 00000000..a5e9d27b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.html @@ -0,0 +1 @@ +

feedback works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.spec.ts new file mode 100644 index 00000000..8d80312d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { FeedbackComponent } from './feedback.component'; + +describe('FeedbackComponent', () => { + let component: FeedbackComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ FeedbackComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(FeedbackComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.ts new file mode 100644 index 00000000..b0407b10 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/feedback/feedback.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-feedback', + templateUrl: './feedback.component.html', + styleUrls: ['./feedback.component.scss'] +}) +export class FeedbackComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incident.model.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incident.model.spec.ts new file mode 100644 index 00000000..aafb17af --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incident.model.spec.ts @@ -0,0 +1,7 @@ +import { Incident } from './incident.model'; + +describe('Incident', () => { + it('should create an instance', () => { + expect(new Incident()).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incident.model.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incident.model.ts new file mode 100644 index 00000000..33d40791 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incident.model.ts @@ -0,0 +1,130 @@ +export interface IState { + id: number; + name: string; + isSettable: boolean; +} + +export var states: IState[] = [ + { id: 0, name: "New", isSettable: false }, + { id: 1, name: "Assigned", isSettable: true }, + { id: 2, name: "Ignored", isSettable: true }, + { id: 3, name: "Closed", isSettable: true }, + { id: 4, name: "Re-opened", isSettable: false } +]; + +export class Incident { + applicationId: number; + assignedAtUtc: Date | null; + assignedTo: string; + assignedToId: number | null; + contextCollections: string[]; + createdAtUtc: Date; + description: string; + facts: IQuickFact[]; + fullName: string; + id: number; + state: number; + isIgnored: boolean; + isReOpened: boolean; + isSolved: boolean; + isAssigned: boolean; + lastReportReceivedAtUtc: Date; + previousSolutionAtUtc: Date; + reOpenedAtUtc: Date; + reportCount: number; + solution: string; + solvedAtUtc: Date; + stackTrace: string; + tags: string[]; + updatedAtUtc: Date; + latestRefresh: Date; + suggestedSolutions: ISuggestedSolution[]; + highlightedContextData: IHighlightedData[]; + monthReports: IncidentMonthStats; +} + +/** + * + */ +export class IncidentMonthStats { + days: string[]; + values: number[]; +} + +export class IncidentRecommendation { + applicationId: number; + applicationName: string; + createdAtUtc: Date; + id: number; + name: string; + reportCount: number; + lastReportReceivedAtUtc: Date; + weight: number; + exceptionTypeName: string; + motivation: string; +} + +export class IncidentSummary { + applicationId: number; + applicationName: string; + assignedAtUtc: Date | null; + createdAtUtc: Date; + id: number; + isReOpened: boolean; + name: string; + reportCount: number; + lastReportReceivedAtUtc: Date; + + isIgnored: boolean; + isSolved: boolean; + isAssigned: boolean; + assignedTo: string; + assignedToId: number | null; +} + +export interface ISuggestedSolution { + reason: string; + suggestedSolution: string; +} + +export interface IHighlightedData { + description: string; + name: string; + url: string; + value: string[]; +} + +export interface IQuickFact { + description: string; + title: string; + url: string; + value: string; +} + +export enum IncidentState { + /** + * Incident have arrived but have not yet been categorized. + */ + New = 0, + + /** + * Incident should be fixed (assigned) + */ + Active = 1, + + + /** + * Ignore all reports for this incident + */ + Ignored = 2, + + /** + * Incident have been corrected. + */ + Closed = 3, + + /** + * We received a new error report on a closed incident (used in history table) + */ + ReOpened = 4, +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.module.ts new file mode 100644 index 00000000..ea27f684 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.module.ts @@ -0,0 +1,93 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SearchComponent } from './search/search.component'; +import { DetailsComponent } from './details/details.component'; +import { RouterModule, Routes } from '@angular/router'; +import { ReportsComponent } from './details/reports/reports.component'; +import { BugReportsComponent } from './details/bugreports/bugreports.component'; +import { OriginsComponent } from './details/origins/origins.component'; +import { LogsComponent } from './details/logs/logs.component'; +import { ImpactComponent } from './details/impact/impact.component'; +import { HomeComponent } from './details/home/home.component'; +import { PipeModule } from "../../pipes/pipe.module"; +import { RecommendComponent } from './recommend/recommend.component'; +import { IncidentStubsComponent } from "./stubs/stubs.component"; +import { FeedbackComponent } from './feedback/feedback.component'; +import { InsightChartComponent } from "./charts/insights/insightchart.component"; +import { CloseComponent } from './details/close/close.component'; +import { FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { ControlsModule } from "../_controls/controls.module"; +import { NavbarComponent } from './details/navbar/navbar.component'; + +const incidentsRoutes: Routes = [ + { path: 'application/:applicationId/errors', component: SearchComponent }, + { + path: 'application/:applicationid/error/:incidentId', + component: DetailsComponent, + children: [ + { + path: '', + component: HomeComponent + }, + { + path: 'home', + component: HomeComponent + }, + { + path: 'impact', + component: ImpactComponent + }, + { + path: 'origins', + component: OriginsComponent + }, + { + path: 'close', + component: CloseComponent + }, + { + path: 'logs', + component: LogsComponent + }, + { + path: 'users', + component: BugReportsComponent + } + ] + } +]; + + +@NgModule({ + declarations: [ + IncidentStubsComponent, + SearchComponent, + DetailsComponent, + ReportsComponent, + BugReportsComponent, + OriginsComponent, + LogsComponent, + ImpactComponent, + HomeComponent, + RecommendComponent, + FeedbackComponent, + InsightChartComponent, + CloseComponent, + NavbarComponent + ], + imports: [ + PipeModule, + CommonModule, + ControlsModule, + FormsModule, + ReactiveFormsModule, + RouterModule.forChild(incidentsRoutes) + ], + exports: [ + RouterModule, + IncidentStubsComponent, + InsightChartComponent, + CloseComponent + ] +}) +export class IncidentsModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.service.spec.ts new file mode 100644 index 00000000..8c621fb9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { IncidentsService } from './incidents.service'; + +describe('IncidentsService', () => { + let service: IncidentsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(IncidentsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.service.ts new file mode 100644 index 00000000..7de382d0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/incidents.service.ts @@ -0,0 +1,247 @@ +import { Injectable, OnDestroy } from '@angular/core'; +import { AuthorizeService } from "../../api-authorization/authorize.service"; +import * as api from "../../server-api/Core/Incidents"; +import { IHubEvent, ISubscriber, SignalRService } from "../services/signal-r.service"; +import { ApiClient } from "../utils/HttpClient"; +import { Incident, IncidentRecommendation, IncidentSummary, IncidentState } from "./incident.model"; +import { IncidentLoader } from "./IncidentConverter"; +import { ToastrService } from "ngx-toastr"; +import SortOrder = api.SortOrder; + +interface IIncidentMap { + item: Incident | null; + id: number; + refreshedAt: Date; + loadPromise: Promise; + +} + +@Injectable({ + providedIn: 'root' +}) +export class IncidentsService implements ISubscriber, OnDestroy { + private cachedIncidents: IIncidentMap[] = []; + private myIncidentsList: IncidentSummary[] = []; + private incidentLoader: IncidentLoader; + private initPromise: Promise; + + constructor( + private readonly apiClient: ApiClient, + private readonly signalR: SignalRService, + private readonly authService: AuthorizeService, + private readonly toastrService: ToastrService) { + + this.incidentLoader = new IncidentLoader(apiClient); + + this.initPromise = new Promise((accept, reject) => { + this.incidentLoader.findForUser(authService.user.accountId) + .then(result => { + this.myIncidentsList = result; + accept(null); + }).catch(e => { + reject(e); + }); + }); + + signalR.subscribe(x => { + return x.typeName === "IncidentAssigned" || x.typeName === "IncidentCreated" || x.typeName === "IncidentClosed"; + }, this); + this.incidentLoader = new IncidentLoader(apiClient); + } + + + /** + * + * @param incidentId + * @param accountId -1 = logged om user + */ + async assign(incidentId: number, accountId: number): Promise { + + if (!incidentId) { + throw new Error("incidentId must be specified"); + } + + if (accountId === -1) { + accountId = this.authService.user.accountId; + } + + if (!accountId) { + throw new Error("accountId must be specified"); + } + + const cmd = new api.AssignIncident(); + cmd.incidentId = incidentId; + cmd.assignedTo = accountId; + await this.apiClient.command(cmd); + + var incident = await this.get(incidentId); + this.incidentLoader.refreshIncident(incident); + + if (accountId === this.authService.user.accountId) { + this.incidentLoader.findForUser(accountId) + .then(x => { + var newIncident = x.find(y => y.id === incidentId); + if (newIncident) { + this.myIncidentsList.push(newIncident); + } + }); + } + + return null; + } + + /** + * + * @param incidentId + * @param version + * @param reason + * @return Number of incident followers. + */ + async close(incidentId: number, version: string, reason: string): Promise { + var cmd = new api.CloseIncident(); + cmd.incidentId = incidentId; + cmd.applicationVersion = version; + cmd.solution = reason; + await this.apiClient.command(cmd); + + const q = new api.GetIncidentForClosePage(); + q.incidentId = incidentId; + var result = await this.apiClient.query(q); + //return result.SubscriberCount; + return 1; + } + + get myIncidents(): IncidentSummary[] { + return this.myIncidentsList; + } + + initialize(): Promise { + return this.initPromise; + } + + async getRecommendations(applicationId?: number): Promise { + return await this.incidentLoader.getRecommendations(applicationId); + } + + async getLatest(applicationId?: number, count: number = 3): Promise { + var q = new api.FindIncidents(); + if (applicationId > 0) { + q.applicationIds = [applicationId]; + } + q.sortType = SortOrder.Newest; + q.isNew = true; + q.pageNumber = 1; + q.itemsPerPage = 3; + + + var result: IncidentSummary[] = []; + var reply = await this.apiClient.query(q); + reply.items.forEach(x => { + var item = new IncidentSummary(); + item.applicationId = x.applicationId; + item.applicationName = x.applicationName; + item.createdAtUtc = new Date(x.createdAtUtc + "Z"); + item.id = x.id; + item.lastReportReceivedAtUtc = new Date(x.lastReportReceivedAtUtc + "Z"); + item.name = x.name; + item.reportCount = x.reportCount; + result.push(item); + }); + + return result; + } + + async get(incidentId: number): Promise { + if (!incidentId) { + throw new Error("incidentId is required."); + } + + let wrapper = this.cachedIncidents.find(x => x.item && x.id === incidentId); + if (wrapper) { + if (wrapper.item === null) { + await wrapper.loadPromise; + } + + //TODO: Check refreshTime + return wrapper.item; + } + + wrapper = { + id: incidentId, + item: null, + loadPromise: null, + refreshedAt: new Date() + }; + this.cachedIncidents.push(wrapper); + + const incident = new Incident(); + incident.id = incidentId; + wrapper.loadPromise = this.incidentLoader.refreshIncident(incident, () => { + wrapper.item = incident; + }); + + //TODO: Push it to our list. + //if (incident.assignedToId === this.authService.user.accountId) { + // this.myIncidentsList.push(incident); + //} + + return await wrapper.loadPromise; + } + + ngOnDestroy(): void { + this.signalR.unsubscribe(this); + } + + handle(event: IHubEvent) { + this.handleAsync(event); + } + + private async handleAsync(event: IHubEvent): Promise { + switch (event.typeName) { + case api.IncidentAssigned.TYPE_NAME: + const incidentAssigned = event.body; + const incident = await this.findIncident(incidentAssigned.incidentId); + if (incident) { + incident.assignedAtUtc = new Date(); + incident.assignedToId = incidentAssigned.assignedToId; + incident.isAssigned = true; + incident.assignedTo = "apa"; //TODO: Lookup + + } else if (incidentAssigned.assignedToId === this.authService.user.accountId) { + this.get(incidentAssigned.incidentId); + } + + case api.IncidentIgnored.TYPE_NAME: + const incidentIgnored = event.body; + this.cachedIncidents = this.cachedIncidents.filter(x => x.id !== incidentIgnored.incidentId); + break; + + case api.IncidentCreated.TYPE_NAME: + const incidentCreated = event.body; + this.get(incidentCreated.incidentId); + const link = `/application/${incidentCreated.applicationId}/error/${incidentCreated.incidentId}`; + this.toastrService.info(`A new error '${incidentCreated.incidentName} was received.`, "New error", { enableHtml: true }); + + default: + break; + } + } + + + private async findIncident(incidentId: number): Promise { + var incident = this.cachedIncidents.find(x => x.id === incidentId); + if (!incident) { + return null; + } + + await incident.loadPromise; + return incident.item; + } + + + ignore(incidentId: number) { + var cmd = new api.IgnoreIncident(); + cmd.incidentId = incidentId; + this.apiClient.command(cmd); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.html new file mode 100644 index 00000000..3a335fcb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.html @@ -0,0 +1 @@ +

recommend works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.spec.ts new file mode 100644 index 00000000..dae60d6a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RecommendComponent } from './recommend.component'; + +describe('RecommendComponent', () => { + let component: RecommendComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ RecommendComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(RecommendComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.ts new file mode 100644 index 00000000..6eff3d5e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/recommend/recommend.component.ts @@ -0,0 +1,17 @@ +import { Component, OnInit } from '@angular/core'; +import { IncidentsService } from "../incidents.service"; + +@Component({ + selector: 'app-recommend', + templateUrl: './recommend.component.html', + styleUrls: ['./recommend.component.scss'] +}) +export class RecommendComponent implements OnInit { + + constructor(private incidentService: IncidentsService) { + } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/report.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/report.service.spec.ts new file mode 100644 index 00000000..ea0ae5ad --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/report.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ReportService } from './report.service'; + +describe('ReportService', () => { + let service: ReportService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ReportService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/report.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/report.service.ts new file mode 100644 index 00000000..874c3b36 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/report.service.ts @@ -0,0 +1,116 @@ +import { Injectable } from '@angular/core'; +import { ApiClient } from "../utils/HttpClient"; +import * as api from "../../server-api/Core/Reports"; + +export class ReportSummary { + public createdAtUtc: string; + public id: number; + public message: string; + public remoteAddress: string; +} + +export class Report { + public contextCollections: ReportCollection[]; + public createdAtUtc: string; + public emailAddress: string; + public errorId: string; + public exception: ReportException; + public id: string; + public incidentId: string; + public message: string; + public stackTrace: string; + public userFeedback: string; +} + +export class ReportCollection { + public name: string; + public properties: KeyValuePair[]; +} +export class KeyValuePair { + public key: string; + public value: string; +} + +export class ReportException { + public assemblyName: string; + public baseClasses: string[]; + public fullName: string; + public innerException: ReportException; + public message: string; + public name: string; + public namespace: string; + public stackTrace: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class ReportService { + + constructor(private readonly apiClient: ApiClient) { } + + async getReport(id: number): Promise { + const query = new api.GetReport(); + query.reportId = id; + + const dto = await this.apiClient.query(query); + const entity = new Report(); + entity.id = dto.id; + entity.createdAtUtc = dto.createdAtUtc; + entity.contextCollections = this.convertCollections(dto.contextCollections); + entity.emailAddress = dto.emailAddress; + entity.errorId = dto.errorId; + entity.exception = this.convertException(dto.exception); + entity.incidentId = dto.incidentId; + entity.message = dto.message; + entity.stackTrace = dto.stackTrace; + entity.userFeedback = dto.userFeedback; + return entity; + } + + async getReportList(incidentId: number, pageNumber = 1, pageSize = 20): Promise { + const query = new api.GetReportList(); + query.incidentId = incidentId; + query.pageNumber = pageNumber; + query.pageSize = pageSize; + + const dto = await this.apiClient.query(query); + + return dto.items.map(dtoItem => { + var item = new ReportSummary(); + item.id = dtoItem.id; + item.createdAtUtc = dtoItem.createdAtUtc; + item.message = dtoItem.message; + item.remoteAddress = dtoItem.remoteAddress; + return item; + }); + } + + private convertCollections(dto: api.GetReportResultContextCollection[]): ReportCollection[] { + return dto.map(value => { + var col = new ReportCollection(); + col.name = value.name; + col.properties = value.properties.map(prop => { + var p = new KeyValuePair(); + p.key = prop.key; + p.value = prop.value; + return p; + }); + return col; + }); + } + private convertException(dto: api.GetReportException): ReportException { + var col = new ReportException(); + col.name = dto.name; + col.message = dto.message; + col.assemblyName = dto.assemblyName; + col.fullName = dto.fullName; + if (dto.innerException != null) { + col.innerException = this.convertException(dto.innerException); + } + + col.namespace = dto.namespace; + + return col; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.html new file mode 100644 index 00000000..70513ce2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.html @@ -0,0 +1,75 @@ +
+
+ +
+

Stack trace

+
+
{{incident.stackTrace}}
+
+ +

+ Report browser + (current: {{ currentReport.createdAtUtc|ago:'never':'full' }}) +

+
+
+ + + + + + +
{{prop.key}}{{prop.value}}
+
+
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +

Product impact

+
+ + + + + +
{{fact.title}}
+
+

Reports

+
+
+
+ +

Affect

+
+ +
+ +
+
+ +
+ + +
+

Choose error report

+
+ +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.scss new file mode 100644 index 00000000..7bab6f3e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.scss @@ -0,0 +1 @@ +@import "../details.component.scss"; diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.spec.ts new file mode 100644 index 00000000..2c5a1726 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { HomeComponent } from './home.component'; + +describe('HomeComponent', () => { + let component: HomeComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ HomeComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(HomeComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.ts new file mode 100644 index 00000000..ba1d2411 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/home/home.component.ts @@ -0,0 +1,143 @@ +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import { Incident } from "../../incident.model"; +import { IncidentsService } from "../../incidents.service"; +import { ActivatedRoute } from "@angular/router"; +import { ReportService, ReportSummary, Report, ReportCollection } from "../../report.service"; +import { ChartService } from "../../../services/chart.service"; +import { ModalService } from "../../../_controls/modal/modal.service"; + +@Component({ + selector: 'incident-details-home', + templateUrl: './home.component.html', + styleUrls: ['./home.component.scss'] +}) +export class HomeComponent implements OnInit, OnDestroy { + private sub: any; + incidentId: number; + applicationId: number; + incident = new Incident(); + reports: ReportSummary[] = []; + currentReport: Report; + currentCollection: ReportCollection; + currentCollectionName: string; + canShowNext = true; + canShowPrevious = true; + reportIndex = 0; + + constructor( + private incidentService: IncidentsService, + private route: ActivatedRoute, + private chartService: ChartService, + private reportService: ReportService, + private modalService: ModalService + ) { + + } + + ngOnInit(): void { + this.sub = this.route.parent.params.subscribe(params => { + this.incidentId = +params['incidentId']; + this.loadEverything(); + }); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + private async loadEverything() { + this.incident = await this.incidentService.get(this.incidentId); + this.applicationId = this.incident.applicationId; + const labels = this.chartService.generateLabelsFromStringDate(this.incident.monthReports.days, { month: 'short', day: 'numeric' }); + this.chartService.drawLineChart('dayReports', + { + labels: labels + }, + [ + { + name: 'Received reports', + data: this.incident.monthReports.values + } + ]); + + this.reports = await this.reportService.getReportList(this.incidentId); + if (this.reports.length > 0) { + this.selectReport(this.reports[0].id); + } + + } + + + selectReport(reportId: number) { + this.reportService.getReport(reportId) + .then(report => { + this.currentReport = report; + if (report.contextCollections.length > 0) { + let collection = report.contextCollections.filter(x => x.name === this.currentCollectionName); + if (collection.length > 0) { + this.selectCollection(collection[0].name); + return; + } + collection = report.contextCollections.filter(x => x.name === 'CustomData'); + if (collection.length > 0) { + this.selectCollection(collection[0].name); + return; + } + collection = report.contextCollections.filter(x => x.name === 'ExceptionProperties'); + if (collection.length > 0) { + this.selectCollection(collection[0].name); + return; + } + + this.selectCollection(report.contextCollections[0].name); + } + } + ); + + this.canShowNext = true; + this.canShowPrevious = true; + if (this.reportIndex === 0) { + this.canShowPrevious = false; + } + if (this.reportIndex === this.reports.length - 1) { + this.canShowNext = false; + } + } + + + showNextReport() { + if (this.reportIndex < this.reports.length - 1) { + this.reportIndex++; + this.selectReport(this.reports[this.reportIndex].id); + } + + } + + showPreviousReport() { + if (this.reportIndex > 0) { + this.reportIndex--; + this.selectReport(this.reports[this.reportIndex].id); + } + + } + + + showReportSelector() { + this.modalService.open("chooseReportModal"); + } + selectSpecificReport(report: ReportSummary) { + this.selectReport(report.id); + this.modalService.close("chooseReportModal"); + } + + selectCollection(name: string) { + const collection = this.currentReport.contextCollections.filter(x => x.name === name); + if (collection.length > 0) { + this.currentCollectionName = name; + this.currentCollection = collection[0]; + } else { + this.currentCollectionName = null; + this.currentCollection = null; + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.html new file mode 100644 index 00000000..e3086dde --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.html @@ -0,0 +1,31 @@ + + + + +
+

Close error

+
+ + +
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.scss new file mode 100644 index 00000000..5afcc4f4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.scss @@ -0,0 +1,100 @@ +@import "../../../../styles/_partials/coderr-variables.scss"; +@import "../../../../styles/_partials/_mixins.scss"; + +.submenu { + color: #ddd; + background: $nav-sub-bg; + padding-top: 10px; + padding-left: 15px; + padding-right: 15px; + + .groups { + margin-top: 10px; + margin-bottom: 10px; + + a { + background-color: $nav-sub-tab; + padding: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + } + } + + a { + color: $nav-text; + text-decoration: none; + } + + .application-list { + display: flex; + flex-direction: column; + } + + .application-list div { + padding: 5px; + } + + .state { + margin-top: 10px; + padding-bottom: 10px; + + dl { + display: inline; + + dt { + display: inline; + color: #999; + margin-right: 5px; + } + + dd { + display: inline; + margin-left: 0; + margin-right: 10px; + + select { + border-radius: 3px; + padding: 2px; + background-color: rgba(0, 0, 0, 0.3); + border-color: rgba(255, 255, 255, 0.1); + color: #ddd; + } + } + } + + .tags { + display: inline; + + span { + border-radius: 2px; + padding: 2px; + font-size: 0.8em; + background-color: darken($blue, 40%); + margin-right: 5px; + } + } + } + + .actions { + margin-left: auto; + margin-bottom: 3px; + margin-top: auto; + + a { + color: darken($blue, 50%); + padding: 3px 5px; + border-top: 1px solid darken($blue, 10%); + border-left: 1px solid darken($blue, 10%); + border-right: 1px solid darken($blue, 10%); + background-color: $blue; + + &.active { + color: lighten($blue, 50%); + } + + &:hover { + color: $red; + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.spec.ts new file mode 100644 index 00000000..f8ccd6f4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NavbarComponent } from './navbar.component'; + +describe('NavbarComponent', () => { + let component: NavbarComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ NavbarComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(NavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.ts new file mode 100644 index 00000000..613959c5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/navbar/navbar.component.ts @@ -0,0 +1,83 @@ +import { Component, OnInit, Input } from '@angular/core'; +import { IncidentsService } from "../../incidents.service"; +import { IncidentState, Incident, states, IState } from "../../incident.model"; +import { ModalService } from "../../../_controls/modal/modal.service"; +import { ToastrService } from "ngx-toastr"; +import { AccountService, User } from "../../../accounts/account.service"; + +@Component({ + selector: 'search-navbar', + templateUrl: './navbar.component.html', + styleUrls: ['./navbar.component.scss'] +}) +export class NavbarComponent implements OnInit { + incident = new Incident(); + states = states; + myIncidentId: number; + users: User[] = []; + + constructor( + private incidentService: IncidentsService, + private modalService: ModalService, + private toastrService: ToastrService, + private accountService: AccountService + ) { + } + + ngOnInit(): void { + } + + @Input() + get incidentId(): number { return this.myIncidentId; } + set incidentId(incidentId: number) { + this.myIncidentId = incidentId; + if (this.myIncidentId > 0) { + this.loadEverything(); + } + } + + + assignIncident(accountId: number) { + this.incidentService.assign(this.incidentId, +accountId); + this.incident.state = IncidentState.Active; + this.toastrService.success("Error has been assigned."); + } + + changeState(stateId: number) { + var state = this.states[stateId]; + + switch (state.id) { + case IncidentState.Active: + this.assignIncident(-1); + break; + + case IncidentState.Closed: + this.close(); + break; + + case IncidentState.Ignored: + this.incidentService.ignore(this.incidentId); + this.toastrService.success("Future reports of this error will be ignored."); + break; + + default: + this.toastrService.warning(`Changing state to ${state.name} is invalid when state is ${this.incident.state}.`); + } + + } + + close() { + this.modalService.open("closeIncidentModal"); + + } + + closeCloseModal(evt: any) { + this.modalService.close("closeIncidentModal"); + + } + + private async loadEverything() { + this.incident = await this.incidentService.get(this.myIncidentId); + this.users = await this.accountService.getAll(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.html new file mode 100644 index 00000000..0f36bff7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.html @@ -0,0 +1,142 @@ + + +
+ + + + + + + + + + + + + + + + + + + + +
NameCreated Last report Report count
+ + {{incident.name}} + + {{incident.createdAtUtc|ago}} + + {{incident.lastReportReceivedAtUtc|ago}} + + {{incident.reportCount}} +
+ There is nothing here, go away. err.. The search that you requested returned nothing. Have a great day. +
+
+ + +
+
+

Save search

+
+

+ Replace an existing search tab or save the search in a new tab. +

+ + + + + + + + + +
Tab name +
+
Replace: + +
+
+
+ + +
+
+
+
+ +
+
+

Delete search

+
+ +
+
+ + +
+
+
+
+ +

Search tabs

+

You can save frequently used searches here, allowing you to quickly navigate between your errors.

+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.scss new file mode 100644 index 00000000..c7f7435f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.scss @@ -0,0 +1,131 @@ +@import "../../../styles/_partials/coderr-variables.scss"; +@import "../../../styles/_partials/_mixins.scss"; + +.sortable { + cursor: pointer; +} +td, th { + white-space: nowrap; + min-width: 130px; +} + +td.a, th.a { + min-width: 150px; +} + +.submenu { + color: #ddd; + background: $nav-sub-bg; + padding-top: 10px; + padding-left: 15px; + padding-right: 15px; + + .contextCollections { + display: inline-block; + } + + input, select { + padding: 5px 2px; + margin-right: 5px; + display: inline-block; + } + + .btn { + margin-top: 5px; + } + + a { + color: $nav-text; + text-decoration: none; + } + + .application-list { + display: flex; + flex-direction: column; + } + + .application-list div { + padding: 5px; + } + + .state { + padding-bottom: 10px; + + dl { + display: inline; + + dt { + display: inline; + color: #999; + margin-right: 5px; + } + + dd { + display: inline; + margin-left: 0; + margin-right: 10px; + + select { + border-radius: 3px; + padding: 2px; + background-color: rgba(0, 0, 0, 0.3); + border-color: rgba(255, 255, 255, 0.1); + color: #ddd; + } + } + } + + .tags { + display: inline; + + span { + border-radius: 2px; + padding: 2px; + font-size: 0.8em; + background-color: darken($blue, 40%); + margin-right: 5px; + } + } + } + + .actions { + margin-left: auto; + margin-bottom: 3px; + margin-top: auto; + white-space: nowrap; + + .buttons { + padding: 0; + margin: 0; + display: inline-block; + + button { + background: transparent; + border: 0; + color: $light; + } + } + + a { + color: darken($blue, 50%); + padding: 3px 5px; + border-top: 1px solid darken($blue, 10%); + border-left: 1px solid darken($blue, 10%); + border-right: 1px solid darken($blue, 10%); + background-color: $blue; + + &.active { + color: lighten($blue, 50%); + } + + &:hover { + color: $red; + } + } + } +} + +table tbody tr td a { + color: #333; + text-decoration: underline; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.spec.ts new file mode 100644 index 00000000..918ce707 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SearchComponent } from './search.component'; + +describe('SearchComponent', () => { + let component: SearchComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ SearchComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.ts new file mode 100644 index 00000000..cf18f42a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/search/search.component.ts @@ -0,0 +1,429 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute } from "@angular/router"; +import { WorkItemService, IIntegration } from "../../services/work-item.service"; +import { FindIncidents, FindIncidentsResult, SortOrder } from "../../../server-api/Core/Incidents"; +import { ApiClient } from "../../utils/HttpClient"; +import { GetEnvironments, GetEnvironmentsResult } from "../../../server-api/Core/Environments"; +import { GetTags, TagDTO } from "../../../server-api/Modules/Tagging"; +import { AccountService } from "../../accounts/account.service"; +import { states } from "../incident.model"; +import { ModalService } from "../../_controls/modal/modal.service"; +import { NavMenuService } from "../../nav-menu/nav-menu.service"; +import { ApplicationService } from "../../applications/application.service"; + +interface Incident { + id: number; + applicationId: number, + applicationName: string, + name: string; + createdAtUtc: Date; + lastReportReceivedAtUtc: Date; + reportCount: number; +} + +interface Application2 { + id: number; + name: string; +} + +interface IListItem { + id: number; + name: string; +} + +interface IListItemWithSelection extends IListItem { + selected: boolean; +} + +class SearchSettings { + tabName: string; + + userId: number = 0; + environmentId: number = 0; + selectedTags: string[] = []; + freeText = ''; + state = 0; + contextCollectionName = ''; + contextCollectionProperty = ''; + contextCollectionPropertyValue = ''; + + sortKey = 1; + ascendingSort = false; +} + + +@Component({ + selector: 'app-search', + templateUrl: './search.component.html', + styleUrls: ['./search.component.scss'] +}) +export class SearchComponent implements OnInit, OnDestroy { + showFilters = false; + showApplicationColumn = false; + + canSave = false; + haveSavedSearches = false; + newTabName = ''; + + /** + * Selected in the save dropdown. + */ + selectedTabName = ''; + + selectedTags = ''; + + settings: SearchSettings = new SearchSettings(); + storedSearches: SearchSettings[] = []; + applicationId: number; + + states = states.map(x => { + return { + id: x.id, + name: x.name, + selected: false + } + }); + + sortColumn = 'created'; + sortAscending = true; + + //data + incidents: Incident[] = []; + tags: IListItemWithSelection[] = []; + environments: IListItem[] = []; + users: IListItem[] = []; + + // incidents to assign + checkedIncidents: number[] = []; + + + //for the close dialog + currentIncidentId = 0; + + + workItemIntegration: IIntegration = { title: '', name: '' }; + haveWorkItemIntegration: boolean | null = null; + showCreateWorkItemButton = false; + + private sub: any; + + + constructor( + private route: ActivatedRoute, + private apiClient: ApiClient, + private workItemService: WorkItemService, + private accountService: AccountService, + private modalService: ModalService, + private appService: ApplicationService, + private navService: NavMenuService) { + } + + ngOnInit(): void { + this.sub = this.route.params.subscribe(params => { + this.applicationId = +params['applicationId']; + var p1 = this.loadEnvironments(); + var p2 = this.loadTags(); + var p3 = this.loadUsers(); + + Promise.all([p1, p2, p3]).then(x => { + this.loadSearches(); + this.searchInternal(true); + }); + + }); + + + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + search() { + this.searchInternal(false); + } + + sort(columnName: string) { + if (this.sortColumn === columnName) { + this.sortAscending = !this.sortAscending; + } else { + this.sortColumn = columnName; + this.sortAscending = true; + } + + this.settings.ascendingSort = this.sortAscending; + + switch (columnName) { + case 'created': + this.settings.sortKey = SortOrder.Newest; + break; + case 'lastReport': + this.settings.sortKey = SortOrder.LatestReport; + break; + case 'reportCount': + this.settings.sortKey = SortOrder.ReportCount; + break; + } + this.searchInternal(false); + } + + getSortClass(columnName: string): string { + if (columnName === this.sortColumn) { + return this.sortAscending ? 'fa fa-chevron-up' : 'fa fa-chevron-down'; + } + + return ''; + } + + reset() { + this.settings = new SearchSettings(); + this.newTabName = ''; + this.sortColumn = 'created'; + this.sortAscending = true; + this.searchInternal(false); + } + + private async loadEnvironments(): Promise { + const q = new GetEnvironments(); + const response = await this.apiClient.query(q); + this.environments.length = 0; + response.items.forEach(x => { + this.environments.push({ id: x.id, name: x.name }); + }); + + var app = await this.appService.get(this.applicationId); + this.navService.updateNav([ + { title: app.name, route: ['application', app.id] }, + { title: 'Errors', route: ['application', app.id, 'errors'] } + ]); + + } + + private async loadTags(): Promise { + let q = new GetTags(); + var response = await this.apiClient.query(q); + + this.tags.length = 0; + response.forEach(x => { + this.tags.push({ id: 0, name: x.name, selected: false }); + }); + } + + private async loadUsers(): Promise { + var users = await this.accountService.getAllButMe(); + this.users = users.map(x => { + return { + id: x.id, + name: x.userName, + } + }); + } + + private async searchInternal(byCode?: boolean): Promise { + var query = new FindIncidents(); + query.freeText = this.settings.freeText; + + switch (+this.settings.state) { + case -1: + break; + case 0: + query.isNew = true; + break; + case 1: + query.isAssigned = true; + break; + case 3: + query.isClosed = true; + break; + case 2: + query.isIgnored = true; + break; + } + + query.sortAscending = this.settings.ascendingSort; + query.sortType = this.settings.sortKey; + query.tags = this.parseTags(); + + if (this.applicationId > 0) { + query.applicationIds = [this.applicationId]; + } + + if (this.settings.environmentId > 0) { + query.environmentIds = [+this.settings.environmentId]; + } + + if (this.settings.userId > 0) { + query.assignedToId = +this.settings.userId; + } + + query.pageNumber = 1; + query.itemsPerPage = 50; + + if (this.settings.contextCollectionName != null && this.settings.contextCollectionName !== "") { + query.contextCollectionName = this.settings.contextCollectionName; + } + if (this.settings.contextCollectionProperty != null && this.settings.contextCollectionProperty !== "") { + query.contextCollectionPropertyName = this.settings.contextCollectionProperty; + } + if (this.settings.contextCollectionPropertyValue != null && this.settings.contextCollectionPropertyValue !== "") { + query.contextCollectionPropertyValue = this.settings.contextCollectionPropertyValue; + } + + if (!byCode && this.selectedTabName.length > 0) { + this.settings.selectedTags = this.parseTags(); + this.storeSearches(); + } + + var result = await this.apiClient.query(query); + this.incidents.splice(0); + result.items.forEach(item => { + var entity: Incident = { + applicationId: item.applicationId, + applicationName: item.applicationName, + createdAtUtc: item.createdAtUtc, + id: item.id, + lastReportReceivedAtUtc: item.lastReportReceivedAtUtc, + name: item.name, + reportCount: item.reportCount + }; + this.incidents.push(entity); + }); + } + + createWorkItems() { + var incidents = this.getSelectedIncidents(); + incidents.forEach(incidentId => { + this.workItemService.createWorkItem(this.applicationId, incidentId); + }); + + //TODO: toasstr + } + + selectSearch(search: SearchSettings) { + if (!search.selectedTags) { + search.selectedTags = []; + } + + this.settings = search; + this.tags.forEach(x => { + x.selected = search.selectedTags.includes(x.name); + }); + + this.selectedTags = search.selectedTags.join(','); + this.selectedTabName = this.settings.tabName; + this.newTabName = this.settings.tabName; + + this.sortAscending = this.settings.ascendingSort; + switch (this.settings.sortKey) { + case SortOrder.Newest: + this.sortColumn = "created"; + break; + case SortOrder.LatestReport: + this.sortColumn = "lastReport"; + break; + case SortOrder.ReportCount: + this.sortColumn = "reportCount"; + break; + } + + this.search(); + } + + showSaveSearch() { + this.modalService.open("newSearchModal"); + } + + saveSearch() { + // storing a new search + if (this.selectedTabName.length === 0) { + var newSettings = Object.assign({}, this.settings); + newSettings.tabName = this.newTabName; + this.selectedTabName = this.newTabName; + this.settings = newSettings; + this.storedSearches.push(newSettings); + } + + // renaming an existing search + else if (this.newTabName.length > 0 && this.newTabName !== this.selectedTabName) { + this.settings.tabName = this.newTabName; + this.selectedTabName = this.newTabName; + } + + this.settings.selectedTags = this.parseTags(); + this.modalService.close("newSearchModal"); + this.storeSearches(); + this.haveSavedSearches = true; + } + + cancelSaveSearch() { + this.modalService.close("newSearchModal"); + } + + showDeleteSearch() { + this.modalService.open("removeSearchModal"); + } + + deleteSearch() { + this.storedSearches = this.storedSearches.filter(x => x.tabName !== this.selectedTabName); + if (this.selectedTabName === this.settings.tabName) { + this.settings = new SearchSettings(); + } + + this.storeSearches(); + this.haveSavedSearches = true; + this.modalService.close("removeSearchModal"); + } + + cancelRemoveSearch() { + this.modalService.close("removeSearchModal"); + } + + + private parseTags(): string[] { + if (this.selectedTags.length < 2) { + return null; + } + + return this.selectedTags.split(",").map(item => item.trim()); + } + + private async checkApplicationIntegration(applicationId: number) { + var workItemIntegration = await this.workItemService.findIntegration(applicationId); + if (workItemIntegration == null) { + this.workItemIntegration.title = ''; + this.haveWorkItemIntegration = false; + return; + } + this.workItemIntegration = workItemIntegration; + this.haveWorkItemIntegration = true; + } + + private getSelectedIncidents(): number[] { + var incidents: number[] = []; + const elems = document.querySelectorAll('#searchTable tbody input[type="checkbox"]:checked'); + for (let i = 0; i < elems.length; i++) { + const elem = elems[i]; + incidents.push(parseInt(elem.value)); + } + return incidents; + } + + private loadSearches() { + this.haveSavedSearches = false; + var json = localStorage.getItem('searchSettings'); + if (!json) { + return; + } + + this.storedSearches = JSON.parse(json); + this.haveSavedSearches = this.storedSearches.length > 0; + } + + private storeSearches() { + var json = JSON.stringify(this.storedSearches); + localStorage.setItem('searchSettings', json); + } + + + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.html new file mode 100644 index 00000000..f83a0325 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.html @@ -0,0 +1,37 @@ + + + + + + + + + +
+ +
+
+ {{incident.applicationName}} +
+
+ created + {{incident.createdAtUtc|ago}} +
+
+ report count + {{incident.reportCount}} + (latest {{incident.lastReportReceivedAtUtc|ago}}) +
+
+ assigned + {{incident.assignedAtUtc|ago}} +
+
+
{{incident.motivation}}
+
+ There are no errors in this category. +
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.scss new file mode 100644 index 00000000..058984a6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.scss @@ -0,0 +1,36 @@ +@import "../../../styles/_partials/coderr-variables.scss"; + +.stub { + background: white; + width: 100%; + + tr { + td { + padding: 10px; + } + } + + tr:nth-child(even) { + background: rgba(100,100,100,0.05); + } + + .motivation{ + color: $blue; + padding: 15px 0; + } + .data { + display: flex; + justify-content: flex-start; + margin-bottom: 10px; + + + div { + padding-right: 15px; + } + + + span { + color: #999; + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.spec.ts new file mode 100644 index 00000000..e8a94555 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { IncidentStubsComponent } from './stubs.component'; + +describe('IncidentStubsComponent', () => { + let component: IncidentStubsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [IncidentStubsComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(IncidentStubsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.ts new file mode 100644 index 00000000..9a74ec28 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/incidents/stubs/stubs.component.ts @@ -0,0 +1,157 @@ +import { Component, OnInit, OnDestroy, Input } from '@angular/core'; +import { IncidentsService } from "../incidents.service"; +import { SignalRService, ISubscriber, IHubEvent } from "../../services/signal-r.service"; +import * as api from "../../../server-api/Core/Incidents"; + +export enum ItemType { + Mine, + Newest, + Recommended +} + +export class IncidentStub { + id: number; + name: string; + applicationName: string; + applicationId: number; + assignedAtUtc: Date | null; + createdAtUtc: Date; + reportCount: number; + lastReportReceivedAtUtc: Date; + motivation?: string; +} + +@Component({ + selector: 'incident-stubs', + templateUrl: './stubs.component.html', + styleUrls: ['./stubs.component.scss'] +}) +export class IncidentStubsComponent implements ISubscriber, OnInit, OnDestroy { + private _applicationId = 0; + private _itemType = ItemType.Mine; + private allIncidents: IncidentStub[] = []; + private sub: any; + incidents: IncidentStub[] = []; + gotApplicationId = false; + + + constructor( + private readonly incidentService: IncidentsService, + signalR: SignalRService) { + signalR.subscribe(x => { + return x.typeName === "IncidentAssigned" || x.typeName === "IncidentCreated" || x.typeName === "IncidentClosed"; + }, this); + } + + ngOnInit(): void { + + } + + ngOnDestroy(): void { + + } + + handle(event: IHubEvent) { + this.handleAsync(event); + } + + /** + * Should be one of them ItemType enum values. + */ + @Input() + get itemType(): string { return this._itemType.toString(); } + set itemType(itemType: string) { + this._itemType = ItemType[itemType]; + this.load(this._itemType); + } + + @Input() + get applicationId(): number { return this._applicationId; } + set applicationId(applicationId: number) { + this._applicationId = applicationId; + this.gotApplicationId = applicationId > 0; + if (this.gotApplicationId) { + this.incidents = this.allIncidents.filter(x => x.applicationId === this._applicationId); + } else { + this.incidents = this.allIncidents; + } + } + + private async load(itemType: ItemType) { + var log = false; + + switch (itemType) { + + case ItemType.Mine: + await this.loadMine(); + break; + + case ItemType.Recommended: + await this.loadRecommended(this._applicationId); + break; + + default: + await this.loadNewest(this._applicationId); + break; + } + + if (this.gotApplicationId) { + this.incidents = this.allIncidents.filter(x => x.applicationId === this._applicationId); + } else { + this.incidents = this.allIncidents; + } + } + + private async loadMine() { + await this.incidentService.initialize(); + this.allIncidents = this.incidentService.myIncidents; + } + + private async loadRecommended(applicationId?: number) { + var result = await this.incidentService.getRecommendations(applicationId); + + // Not supported in OSS version. + if (!result) { + this.allIncidents = []; + return; + } + + var stubs: IncidentStub[] = []; + result.forEach(x => { + var stub = new IncidentStub(); + stub.id = x.id; + stub.applicationId = x.applicationId; + stub.applicationName = x.applicationName; + stub.createdAtUtc = x.createdAtUtc; + stub.lastReportReceivedAtUtc = x.lastReportReceivedAtUtc; + stub.name = x.name; + stub.reportCount = x.reportCount; + stub.motivation = x.motivation; + stubs.push(stub); + }); + this.allIncidents = stubs; + } + + private async loadNewest(applicationId?: number) { + this.allIncidents = await this.incidentService.getLatest(applicationId, 5); + } + + + + private async handleAsync(event: IHubEvent): Promise { + switch (event.typeName) { + case api.IncidentAssigned.TYPE_NAME: + setTimeout(() => this.load(this._itemType), 1000); + break; + + case api.IncidentCreated.TYPE_NAME: + //timeout so that everything can be processed in the background before we load it. + setTimeout(() => this.load(this._itemType), 5000); + break; + + default: + break; + } + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.html new file mode 100644 index 00000000..d4111386 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.html @@ -0,0 +1,84 @@ +
+
+ + +
+
+
+
+ + + Home + + + / + + {{item.title}} + + + + +
+ +
+
+
+ + +
+

Chat with us

+
+

+ Let us know if you need help or want to discuss code quality. +

+
+
+
+
+
+ +
+ +
+
+
+
diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.scss new file mode 100644 index 00000000..60cbb2aa --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.scss @@ -0,0 +1,41 @@ +@import "../../styles/_partials/coderr-variables.scss"; + +#showGuideTooltip .gotGuide { + animation: pulsingGuide 2s infinite; + background-color: #f3ec78; + background-image: linear-gradient(0deg, $red, $red 50%, $red-60); + background-size: 100%; + -webkit-background-clip: text; + -moz-background-clip: text; + -webkit-text-fill-color: transparent; + -moz-text-fill-color: transparent; +} + +@keyframes test_hover { + from { + background-position: 0% 100%; + } + + to { + background-position: 100% 0%; + } +} + +@keyframes pulsingGuide { + 0% { + background-image: linear-gradient(0deg, $red, $red 70%, $red-60); + } + + 50% { + background-image: linear-gradient(90deg, $red, $red 70%, $red-60); + } + + 75% { + background-image: linear-gradient(180deg, $red, $red 70%, $red-60); + } + + + 100% { + background-image: linear-gradient(270deg, $red, $red 70%, $red-60); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.ts new file mode 100644 index 00000000..bb671d46 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.component.ts @@ -0,0 +1,179 @@ +import { Component, OnDestroy } from "@angular/core"; +import { Router } from "@angular/router"; +import { ToastrService } from "ngx-toastr"; + +import { ApplicationService } from "../applications/application.service"; +import { IApplication } from "../applications/application.model"; +import { INavPill, NavMenuService } from "./nav-menu.service"; +import { GuideService } from "../_controls/guide/guide.service"; +import * as api from "../../server-api/Core/Support"; +import { ModalService } from "../_controls/modal/modal.service"; +import { ApiClient } from "../utils/HttpClient"; +import { AuthorizeService, IUser } from "../../api-authorization/authorize.service"; + +interface IApplicationMenuItem { + id: number, + title: string; + groupIds: number[]; + hasIncidents: boolean; +} + +interface IApplicationMenuGroupItem { + id: number; + title: string; +} + +@Component({ + selector: "app-nav-menu", + templateUrl: "./nav-menu.component.html", + styleUrls: ["./nav-menu.component.scss"] +}) +export class NavMenuComponent implements OnDestroy { + allApplications: IApplicationMenuItem[] = []; + applications: IApplicationMenuItem[]; + groups: IApplicationMenuGroupItem[] = []; + selected: IApplicationMenuItem; + showAsOnboarding = false; + showAppMenu = false; + navItems: INavPill[] = []; + gotGuides: boolean; + + supportMessage = ''; + supportSubject = ''; + + isAuthenticated = false; + + private navSub: any; + private guideSub: any; + private accountSub: any; + + showConfigure = false; + configureAppId: number; + + + private emptyApp = { id: 0, title: "(All applications)", groupIds: [], hasIncidents: false }; + private selectedGroupId: number; + + constructor( + navService: NavMenuService, + private appService: ApplicationService, + private modalService: ModalService, + private guideService: GuideService, + private router: Router, + private apiClient: ApiClient, + private authService: AuthorizeService, + private toastrService: ToastrService) { + appService.selected.subscribe(x => this.onApplicationChanged(x)); + appService.applications.subscribe(x => this.onApplications(x)); + this.navSub = navService.navItems.subscribe(items => this.navItems = items); + this.guideSub = this.guideService.guidesAvailable.subscribe(x => this.onGuidesAvailable(x)); + this.accountSub = this.authService.userEvents.subscribe(x => this.onAccountChange(x)); + } + + toggleAppMenu() { + this.showAppMenu = !this.showAppMenu; + } + + selectApplication(applicationId: number) { + this.showAppMenu = false; + this.appService.selectApplication(applicationId); + + if (!applicationId) { + this.configureAppId = applicationId; + } else if (this.allApplications.length > 0) { + this.configureAppId = this.allApplications[0].id; + } else { + this.configureAppId = 0; + } + + var app = this.allApplications.find(x => x.id === this.configureAppId); + this.showConfigure = app && !app.hasIncidents; + } + + selectGroup(groupId: number) { + this.selectedGroupId = groupId; + this.filterApplications(groupId); + } + + showWizard() { + this.guideService.showNextGuide(); + } + + showSupport() { + this.modalService.open("chatModal"); + } + + sendSupport() { + var cmd = new api.SendSupportRequest(); + cmd.message = this.supportMessage; + cmd.subject = this.supportSubject; + cmd.url = this.router.url; + this.apiClient.command(cmd); + this.modalService.close("chatModal"); + this.toastrService.success("Message has been sent"); + } + + ngOnDestroy(): void { + this.appService.selected.unsubscribe(); + this.appService.applications.unsubscribe(); + this.navSub.unsubscribe(); + this.guideSub.unsubscribe(); + } + + private onAccountChange(user: IUser) { + this.isAuthenticated = user != null; + } + + private onApplicationChanged(application: IApplication) { + if (application == null) { + this.selected = this.emptyApp; + return; + } + + var menuItem = this.toMenuItem(application); + this.showConfigure = !menuItem.hasIncidents; + this.selected = menuItem; + } + + private onApplications(applications: IApplication[]) { + var newApps: IApplicationMenuItem[] = []; + applications.forEach(app => { + newApps.push(this.toMenuItem(app)); + }); + this.allApplications = newApps; + + if (this.allApplications.length > 0 && !this.configureAppId) { + this.configureAppId = this.allApplications[0].id; + } + + var app = this.allApplications.find(x => x.id === this.configureAppId); + this.showConfigure = app && !app.hasIncidents; + + if (this.selectedGroupId > 0) { + this.filterApplications(this.selectedGroupId); + } + } + + private filterApplications(groupId: number) { + var apps: IApplicationMenuItem[]; + if (!groupId) { + apps = this.allApplications; + } else { + apps = this.allApplications.filter(x => x.groupIds.includes(groupId)); + } + this.applications = apps; + + if (apps.indexOf(this.selected) === -1 && apps.length > 0) { + this.selectApplication(apps[0].id); + } + } + + private toMenuItem(app: IApplication): IApplicationMenuItem { + return { id: app.id, title: app.name, groupIds: app.groupIds, hasIncidents: app.totalIncidentCount > 0 }; + } + + private onGuidesAvailable(isAvailable: boolean) { + this.gotGuides = isAvailable; + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.service.spec.ts new file mode 100644 index 00000000..97e9e75b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { NavMenuService } from './nav-menu.service'; + +describe('NavMenuService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: NavMenuService = TestBed.get(NavMenuService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.service.ts new file mode 100644 index 00000000..ee15de48 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/nav-menu/nav-menu.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import * as api from "../../server-api/Core/Incidents"; +import { ApiClient } from "../utils/HttpClient"; + +export interface INavPill { + route: any[]; + title: string; + id?: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class NavMenuService { + selectedApplicationId: BehaviorSubject; + showAsOnboarding: boolean = false; + navItems: BehaviorSubject; + hasIncidents = false; + + constructor(client: ApiClient) { + this.selectedApplicationId = new BehaviorSubject(0); + this.navItems = new BehaviorSubject([]); + var query = new api.FindIncidents(); + query.itemsPerPage = 1; + query.pageNumber = 1; + client.query(query) + .then(result => { + this.hasIncidents = result.totalCount > 0; + }); + + } + + selectApplication(applicationId: number) { + if (!applicationId || applicationId < 0) + throw new Error("Must supply an application id"); + + this.selectedApplicationId.next(applicationId); + } + + updateNav(navItems: INavPill[]) { + this.navItems.next(navItems); + } + + selectGroup(id: number) { + + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/premise.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/premise.module.ts new file mode 100644 index 00000000..240fe492 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/premise.module.ts @@ -0,0 +1,12 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + + + +@NgModule({ + declarations: [], + imports: [ + CommonModule + ] +}) +export class PremiseModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.html new file mode 100644 index 00000000..fa723f3a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.html @@ -0,0 +1 @@ +

add works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.spec.ts new file mode 100644 index 00000000..9d7e2fa8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddComponent } from './add.component'; + +describe('AddComponent', () => { + let component: AddComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ AddComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(AddComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.ts new file mode 100644 index 00000000..489dc284 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/add/add.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-add', + templateUrl: './add.component.html', + styleUrls: ['./add.component.scss'] +}) +export class AddComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.html new file mode 100644 index 00000000..d1393d89 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.html @@ -0,0 +1 @@ +

edit works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.spec.ts new file mode 100644 index 00000000..5a24d45a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EditComponent } from './edit.component'; + +describe('EditComponent', () => { + let component: EditComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ EditComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(EditComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.ts new file mode 100644 index 00000000..415ad84b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/edit/edit.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-edit', + templateUrl: './edit.component.html', + styleUrls: ['./edit.component.scss'] +}) +export class EditComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.html new file mode 100644 index 00000000..7c1fe159 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.html @@ -0,0 +1 @@ +

list works!

diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.spec.ts new file mode 100644 index 00000000..a5d3a5c3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ListComponent } from './list.component'; + +describe('ListComponent', () => { + let component: ListComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ ListComponent ] + }) + .compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(ListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.ts new file mode 100644 index 00000000..011aa130 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/list/list.component.ts @@ -0,0 +1,15 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'] +}) +export class ListComponent implements OnInit { + + constructor() { } + + ngOnInit(): void { + } + +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/team.model.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/team.model.spec.ts new file mode 100644 index 00000000..fda4839d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/team.model.spec.ts @@ -0,0 +1,7 @@ +import { Team } from './team.model'; + +describe('Team', () => { + it('should create an instance', () => { + expect(new Team()).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/team.model.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/team.model.ts new file mode 100644 index 00000000..1d70b59e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/team.model.ts @@ -0,0 +1,2 @@ +export class Team { +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/teams.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/teams.module.ts new file mode 100644 index 00000000..c0b93c08 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/premise/teams/teams.module.ts @@ -0,0 +1,15 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AddComponent } from './add/add.component'; +import { EditComponent } from './edit/edit.component'; +import { ListComponent } from './list/list.component'; + + + +@NgModule({ + declarations: [AddComponent, EditComponent, ListComponent], + imports: [ + CommonModule + ] +}) +export class TeamsModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/HubEntity.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/HubEntity.ts new file mode 100644 index 00000000..3d058388 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/HubEntity.ts @@ -0,0 +1,11 @@ +interface IHubEntity { + Namespace: string; + TypeName: string; + IsEvent: boolean; +} + +interface IHubEvent { + typeName: string; + body: any; + correlationId: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/chart.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/chart.service.spec.ts new file mode 100644 index 00000000..a3a12f61 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/chart.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { ChartService } from './chart.service'; + +describe('ChartService', () => { + let service: ChartService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(ChartService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/chart.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/chart.service.ts new file mode 100644 index 00000000..df52eba4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/chart.service.ts @@ -0,0 +1,114 @@ +import { Injectable } from '@angular/core'; +import ApexCharts from 'apexcharts/dist/apexcharts.common.js' + +export interface IChartSeries { + name: string; + data: any[]; +} + +export interface ILabelOptions { + labels: string[]; + tickCount?: number; +} + +@Injectable({ + providedIn: 'root' +}) +export class ChartService { + private static counter = 1; + private charts: any = {}; + + constructor() { + ApexCharts.colors = ['#9900cc', '#E91E63', '#9C27B0']; + } + + drawLineChart(chartId: string, labels: ILabelOptions, series: IChartSeries[]) { + var height = document.documentElement.clientHeight / 3; + + var options: any = { + series: series, + chart: { + //height: 350, + type: 'line', + }, + stroke: { + width: 7, + curve: 'smooth' + }, + xaxis: { + categories: labels.labels, + }, + height: height + "px", + width: '100%', + fill: { + type: 'gradient', + gradient: { + shade: 'dark', + //gradientToColors: ['#59c1d5'], + shadeIntensity: 1, + type: 'horizontal', + opacityFrom: 1, + opacityTo: 1, + stops: [0, 100] + }, + }, + markers: { + size: 2, + //colors: ["#f18c65", "#f18c99"], + strokeColors: "#fff", + strokeWidth: 2, + hover: { + size: 7, + } + }, + yaxis: { + min: 0, + title: { + text: 'Count', + }, + }, + legend: { + show: true + } + }; + if (labels.tickCount > 0) { + options.xaxis.tickAmount = labels.tickCount; + } + + const chart = new ApexCharts(document.querySelector('#' + chartId), options); + this.charts[chartId] = chart; + chart.render(); + } + + updateLineChart(chartId: string, series: IChartSeries[]) { + //var chart = this.charts[chartId]; + //chart.updateSeries(series); + } + + generateChartId(): string { + return `chart${ChartService.counter++}`; + } + + + generateLabelsFromStringDate(labels: string[], dateFormatOptions = null): string[] { + if (!dateFormatOptions) { + dateFormatOptions = { month: 'short', year: 'numeric' }; + } + + var dates: string[] = []; + labels.forEach(x => { + + // if no timezone was specified, assume UTC. + var prefix = ''; + if (x.indexOf('+') === -1 && x.indexOf('Z') === -1) { + prefix = 'Z'; + } + + const d = new Date(Date.parse(x + prefix)); + + const fmt = new Intl.DateTimeFormat('en-US', dateFormatOptions); + dates.push(fmt.format(d)); + }); + return dates; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/signal-r.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/signal-r.service.spec.ts new file mode 100644 index 00000000..f737fa50 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/signal-r.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SignalRService } from './signal-r.service'; + +describe('SignalRService', () => { + let service: SignalRService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SignalRService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/signal-r.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/signal-r.service.ts new file mode 100644 index 00000000..4d469c73 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/signal-r.service.ts @@ -0,0 +1,164 @@ +import { Injectable } from "@angular/core"; +import * as signalR from "@microsoft/signalr"; +import { Subject, Observable } from 'rxjs'; +declare type CallbackFilter = (evt: IHubEvent) => boolean; +declare type Action = () => void; +declare type Action1 = (arg: T) => void; + +export interface IHubEvent { + typeName: string; + body: any; + correlationId: string; +} + + +interface ICallback { + filter: CallbackFilter; + resolve: Action1; + reject: Action1; + completed: boolean; +} + +interface ICachedMessage { + receivedAtUtc: Date; + message: IHubEvent; +} + +export interface ISubscriber { + handle(event: IHubEvent): void; +} + +interface ISubscriberWrapper { + subscriber: ISubscriber; + filter: CallbackFilter; +} + +@Injectable({ + providedIn: "root" +}) +export class SignalRService { + observer: Subject = new Subject(); + private hubConnection: signalR.HubConnection; + private waitFilters: ICallback[] = []; + private cachedMessages: ICachedMessage[] = []; + private subscribers: ISubscriberWrapper[] = []; + + get eventStream(): Observable { + return this.observer; + } + + subscribe(filter: CallbackFilter, subscriber: ISubscriber) { + this.subscribers.push({ + subscriber: subscriber, + filter: filter, + }); + } + + unsubscribe(subscriber: ISubscriber) { + this.subscribers = this.subscribers.filter(x => x.subscriber !== subscriber); + } + + async wait(filter: CallbackFilter): Promise { + const msg = this.findCachedMessage(filter); + if (msg != null) { + return msg; + } + + var cb: ICallback = { + filter: filter, + reject: null, + resolve: null, + completed: false + }; + this.waitFilters.push(cb); + + return new Promise((accept, reject) => { + cb.reject = x => { + cb.completed = true; + reject(x); + }; + + cb.resolve = evt => { + cb.completed = true; + accept(evt); + }; + + setTimeout(x => { + if (cb.completed) { + return; + } + cb.reject(new Error("Timeout")); + this.waitFilters = this.waitFilters.filter(y => cb !== y); + }, + 5000); + }); + } + + startConnection = () => { + this.hubConnection = new signalR.HubConnectionBuilder() + .withUrl("/hub") + .build(); + this.hubConnection + .start() + .then(() => console.log("Connection started")) + .catch(err => console.log(`Error while starting connection: ${err}`)); + + this.hubConnection.on("OnEvent", (message: IHubEvent) => { + this.cachedMessages.push({ receivedAtUtc: new Date(), message: message }); + + this.resolveFilters(message); + + this + .subscribers + .filter(x => x.filter(message)) + .forEach(x => x.subscriber.handle(message)); + + this.observer.next(message); + }); + }; + + private findCachedMessage(filter: CallbackFilter) { + let matchingCachedMessage: ICachedMessage = null; + + var msgsToRemove: ICachedMessage[] = []; + this.cachedMessages.every(msgWrapper => { + var msDiff = new Date().getTime() - msgWrapper.receivedAtUtc.getTime(); + if (msDiff > 500) { + msgsToRemove.push(msgWrapper); + return true; + } + + if (filter(msgWrapper.message)) { + matchingCachedMessage = msgWrapper; + return false; + } + + return true; + }); + + msgsToRemove.forEach(msg => { + this.cachedMessages.filter(x => x !== msg); + }); + + if (matchingCachedMessage != null) { + return matchingCachedMessage.message; + } + + return null; + } + + private resolveFilters(message: any) { + var filtersToRemove: ICallback[] = []; + + this.waitFilters.forEach(x => { + if (x.filter(message)) { + filtersToRemove.push(x); + x.resolve(message); + } + }); + + filtersToRemove.forEach(x => { + this.waitFilters = this.waitFilters.filter(y => x !== y); + }); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/work-item.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/work-item.service.spec.ts new file mode 100644 index 00000000..f626224c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/work-item.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { WorkItemService } from './work-item.service'; + +describe('WorkItemServiceService', () => { + let service: WorkItemService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(WorkItemService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/work-item.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/work-item.service.ts new file mode 100644 index 00000000..87dced13 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/services/work-item.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import * as WorkItems from "../../server-api/Common/WorkItems"; +import { ApiClient } from "../utils/HttpClient"; + +export interface IWorkItem { + incidentId: number; + applicationId: number; + name: string; + url: string; +} + +export interface IIntegration { + title: string; + name: string; +} +@Injectable({ + providedIn: 'root' +}) +export class WorkItemService { + + constructor(private apiClient: ApiClient) { + + + } + + createWorkItem(applicationId: number, incidentId: number) { + var cmd = new WorkItems.CreateWorkItem(); + cmd.IncidentId = incidentId; + cmd.ApplicationId = applicationId; + this.apiClient.command(cmd); + } + + async getWorkItem(incidentId: number): Promise { + var dto = new WorkItems.FindWorkItem(); + dto.IncidentId = incidentId; + var result = await this.apiClient.query(dto); + if (result == null) + return null; + + var item = { + incidentId, + applicationId: result.ApplicationId, + name: result.Name, + url: result.Url + }; + + return item; + } + + + async findIntegration(applicationId: number): Promise { + var dto = new WorkItems.FindIntegration(); + dto.ApplicationId = applicationId; + var result = await this.apiClient.query(dto); + if (!result.HaveIntegration) { + return null; + } + + return { + title: result.Title, + name: result.Name + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/HttpClient.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/HttpClient.ts new file mode 100644 index 00000000..0a2fffba --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/HttpClient.ts @@ -0,0 +1,182 @@ +import { Injectable } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthorizeGuard } from "../../api-authorization/authorize.guard"; +declare var window: any; + +export interface IRequestOptions { + method?: "GET" | "POST" | "PUT" | "DELETE", + mode?: "cors" | "no-cors", "*cors", "same-origin", + cache: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached", + credentials: "omit" | "same-origin" | "include", + headers: Map, + redirect: 'follow' | "manual" | "*follow" | "error", + referrerPolicy: 'no-referrer' | "*no-referrer-when-downgrade" | "origin" | "origin-when-cross-origin" | "same-origin" | "strict-origin" | "strict-origin-when-cross-origin" | "unsafe-url", + body: any +} + +export interface IHttpResponse { + statusCode: number; + statusReason: string; + contentType: string | null; + body: any; + charset: string | null; +} + +export class HttpError extends Error { + message: string; + reponse: IHttpResponse; + + constructor(response: IHttpResponse) { + super(response.statusReason); + this.message = response.statusReason; + this.reponse = response; + } +} + +@Injectable({ + providedIn: 'root' +}) +export class ApiClient { + private http: HttpClient = new HttpClient(); + private cqsUrl; + private rootUrl; + private static redirected = false; + + constructor(private router: Router) { + var apiRootUrl = '/'; + if (apiRootUrl.substr(apiRootUrl.length - 1, 1) !== "/") + apiRootUrl += '/'; + + this.rootUrl = apiRootUrl; + this.cqsUrl = apiRootUrl + "cqs/"; + } + + async command(cmd: any): Promise { + const headers = { + "X-Cqs-Name": cmd.constructor.TYPE_NAME, + "Content-Type": "application/json", + }; + const json = JSON.stringify(cmd); + await this.http.post(`${this.cqsUrl}?type=${cmd.constructor.TYPE_NAME}`, json, { + headers: headers, + }); + } + + async query(query: any): Promise { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "X-Cqs-Name": query.constructor.TYPE_NAME, + }; + var json = JSON.stringify(query); + var response = await this.http.post(`${this.cqsUrl}?type=${query.constructor.TYPE_NAME}`, json, { headers: headers }); + if (response.statusCode === 401) { + + if (!ApiClient.redirected) { + ApiClient.redirected = true; + } + + + if (AuthorizeGuard.isOpenAccountPage(window.location.pathname)) { + return null; + } + + const loginUrl = localStorage.getItem('loginUrl'); + if (loginUrl) { + if (window.location.pathname.indexOf('account') === -1) { + window.location.href = + loginUrl + "?returnUrl=" + encodeURIComponent(window.location.pathname + window.location.search); + } + + return null; + } + + //this.router.navigate(['account/login'], { queryParams: { returnUrl: window.location.pathname + window.location.search } }); + throw new HttpError(response); + } + ApiClient.redirected = false; + + if (response.statusCode >= 200 && response.statusCode < 300) { + return response.body; + } + if (response.statusCode === 501) { + return null; + } + + throw new Error(response.statusCode + ": " + response.body); + } + + async auth(): Promise { + var result = await this.http.post(`${this.rootUrl}authenticate/`, null); + return result.body; + } +} + +@Injectable({ + providedIn: 'root' +}) +export class HttpClient { + async request(url: string, options?: RequestInit): Promise { + var token = localStorage.getItem('jwt'); + if (token && options) { + options.headers["Authorization"] = "Bearer " + token; + } + + const response = await fetch(url, options); + + if (!response.ok) { + return { + statusCode: response.status, + statusReason: response.statusText, + contentType: response.headers.get('content-type'), + body: await response.text(), + charset: response.headers.get('charset') + } + } + + var body = null; + if (response.status !== 204) { + if (response.headers.get("Content-Type").indexOf('json') > 0) { + body = await response.json(); + } else { + body = await response.text(); + } + } + + return { + statusCode: response.status, + statusReason: response.statusText, + contentType: response.headers.get('content-type'), + body: body, + charset: response.headers.get('charset') + }; + } + + async get(url: string, options?: RequestInit): Promise { + if (!options) { + options = { + method: 'GET', + headers: { 'accept': 'application/json' } + } + } else { + options.method = 'GET'; + } + + return this.request(url, options); + } + + async post(url: string, data: BodyInit, options?: RequestInit): Promise { + if (!options) { + options = { + method: 'POST', + body: data, + headers: { 'content-type': 'application/json' } + } + } else { + options.method = 'POST'; + options.body = data; + } + + return this.request(url, options); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/ModalDialog.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/ModalDialog.ts new file mode 100644 index 00000000..aa0aa3aa --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/ModalDialog.ts @@ -0,0 +1,45 @@ +import { Component, ViewChild, ViewContainerRef, TemplateRef } from "@angular/core"; + + +@Component({ + selector: 'showmodal', + template: ` + + + + + ` +}) +export class ShowModalComponent { + @ViewChild('modal_1') modal: TemplateRef; + @ViewChild('vc', { read: ViewContainerRef }) vc: ViewContainerRef; + backdrop: any + showDialog() { + let view = this.modal.createEmbeddedView(null); + this.vc.insert(view); + this.modal.elementRef.nativeElement.previousElementSibling.classList.remove('fade'); + this.modal.elementRef.nativeElement.previousElementSibling.classList.add('modal-open'); + this.modal.elementRef.nativeElement.previousElementSibling.style.display = 'block'; + this.backdrop = document.createElement('DIV'); + this.backdrop.className = 'modal-backdrop'; + document.body.appendChild(this.backdrop); + } + + closeDialog() { + this.vc.clear(); + document.body.removeChild(this.backdrop); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/SubjectList.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/SubjectList.ts new file mode 100644 index 00000000..133c72d0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/SubjectList.ts @@ -0,0 +1,72 @@ +import { BehaviorSubject, Subject } from "rxjs"; + +export class BehaviorSubjectList +{ + private _items: TEntity[] = []; + private _subject: BehaviorSubject = new BehaviorSubject([]); + private _added: Subject = new Subject(); + private _removed: Subject = new Subject(); + + constructor(private sortFunc: (a: TEntity, b: TEntity) => number, items?: TEntity[]) { + if (items) { + this._items = items.sort(sortFunc); + this.subject.next(items); + } + } + + get current(): TEntity[] { + return this._items; + } + + add(item: TEntity) { + this._items.push(item); + this._items.sort(this.sortFunc); + this._added.next(item); + this._subject.next(this._items); + } + + addAll(items: TEntity[]) { + items.forEach(item => { + this._items.push(item); + }); + this._items.sort(this.sortFunc); + this._subject.next(this._items); + } + + clear() { + this._items.forEach(x => { + this._removed.next(x); + }); + this._items = []; + this._subject.next(this._items); + } + + remove(item: TEntity): boolean { + const index = this._items.indexOf(item, 0); + if (index === -1) { + return false; + } + + this._items.splice(index, 1); + this._removed.next(item); + this._subject.next(this._items); + return true; + } + + get added(): Subject { + return this._added; + } + get removed(): Subject { + return this._removed; + } + + get subject(): BehaviorSubject { + return this._subject; + } + + + + find(predicate: (search: TEntity) => boolean): TEntity { + return this._items.find(predicate); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/modal.dialog.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/modal.dialog.service.ts new file mode 100644 index 00000000..21095f53 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/modal.dialog.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class ModalService { + private modals: any[] = []; + + add(modal: any) { + // add modal to array of active modals + this.modals.push(modal); + } + + remove(id: string) { + // remove modal from array of active modals + this.modals = this.modals.filter(x => x.id !== id); + } + + open(id: string) { + // open modal specified by id + const modal = this.modals.find(x => x.id === id); + modal.open(); + } + + close(id: string) { + // close modal specified by id + const modal = this.modals.find(x => x.id === id); + modal.close(); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/settings.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/settings.service.spec.ts new file mode 100644 index 00000000..359cb6b7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/settings.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { SettingsService } from './settings.service'; + +describe('SettingsService', () => { + let service: SettingsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(SettingsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/settings.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/settings.service.ts new file mode 100644 index 00000000..aa801050 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/utils/settings.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { ApiClient } from "./HttpClient"; +import * as api from "../../server-api/Core/Users"; + +@Injectable({ + providedIn: 'root' +}) +export class SettingsService { + + constructor( + private apiClient: ApiClient) { + + } + + async get(name: string): Promise { + var query = new api.GetAccountSetting(); + query.name = name; + var result = await this.apiClient.query(query); + return result?.value; + } + + async set(name: string, value: string): Promise { + var cmd = new api.SaveAccountSetting(); + cmd.value = value; + cmd.name = name; + await this.apiClient.command(cmd); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/app/validation.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/validation.ts new file mode 100644 index 00000000..7efa4080 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/app/validation.ts @@ -0,0 +1,206 @@ +import "reflect-metadata"; + + +export interface IValidationRule { + evaluate(target: any, value: any, key: string): string | null; +} + +class RequiredValidationRule implements IValidationRule { + static instance = new RequiredValidationRule(); + + evaluate(target: any, value: any, key: string): string | null { + if (value) { + return null; + } + + if (typeof value == "boolean" && value != null) { + return null; + } + + return `${key} is required`; + } +} + +class RangeValidationRule implements IValidationRule { + constructor(private min: number, private max?: number) { + + } + + evaluate(target, value, key: string): string | null { + if (typeof value !== "number") { + return `${key} is not a number and cannot be validated as a range.`; + } + + const valueNumber = value as number; + if (valueNumber < this.min) { + if (this.max > 0) { + return `${key} is currently ${value}, must be between ${this.min} and ${this.max}.`; + } else { + return `${key} is currently ${value}, must be larger or equal to ${this.min}.`; + } + } + + if (this.max > 0 && valueNumber > this.max) { + return `${key} is currently ${value}, must be between ${this.min} and ${this.max}.`; + } + + return null; + } +} + +class StringLengthValidationRule implements IValidationRule { + constructor(private max: number, private min?: number) { + + } + + evaluate(target, value, key: string): string | null { + if (typeof value !== "string") { + return `${key} is not a string and cannot be validated using StringLength.`; + } + + const valueStr = value as string; + if (!value || value.length === 0) { + return null; + } + + + if (valueStr.length > this.max) { + return `${key} must be at most ${this.min} characters.`; + } + + if (this.min && valueStr.length < this.min) { + return `${key} must be more than ${this.min} characters.`; + } + + return null; + } +} + +export interface ICopyOptions { + stringFields?: string[]; + numericFields?: string[]; + booleanFields?: string[]; + exclude?: string[]; + useDestinationProperties?: boolean; + skipExistenceCheck?: boolean; +} + +export function copy(source: any, destination: any, options?: ICopyOptions) { + var propsObj = source; + if (options && options.useDestinationProperties) { + propsObj = destination; + } + +// ReSharper disable once MissingHasOwnPropertyInForeach + for (let key in propsObj) { + + let canPass = options == null || (options.numericFields == null && options.stringFields == null && options.booleanFields == null); + if (options) { + if (options.numericFields && options.numericFields.includes(key)) { + canPass = true; + } + if (options.stringFields && options.stringFields.includes(key)) { + canPass = true; + } + if (options.booleanFields && options.booleanFields.includes(key)) { + canPass = true; + } + if (options.exclude && options.exclude.includes(key)) { + canPass = false; + } + + if (!canPass) { + continue; + } + } + + var skipCheck = options != null && options.skipExistenceCheck; + if (!Object.prototype.hasOwnProperty.call(propsObj, key) && !skipCheck) { + throw new Error(`Could not find '${key}', found fields: ${Object.keys(propsObj).join(', ')}`); + } + + let value = source[key]; + if (options) { + if (options.numericFields && options.numericFields.includes(key) && value != null) { + value = parseInt(value); + } + if (options.booleanFields && options.booleanFields.includes(key) && value != null) { + if (!value || value === "0" || value.toString().toLowerCase() === "false") { + value = false; + } else { + value = true; + } + } + } + + destination[key] = value; + } + +} + +export function required(target: any, propertyKey: string) { + addValidationRule(target, propertyKey, RequiredValidationRule.instance); +} + + +export function range(min: number, max?: number) { + return (target: any, propertyKey: string) => { + addValidationRule(target, propertyKey, new RangeValidationRule(min, max)); + } +} + +export function range2(target: any, propertyKey: string, min: number, max: number) { + return (target: any, propertyKey: string) => { + addValidationRule(target, propertyKey, new RangeValidationRule(min, max)); + } +} + + +export function stringLength(max: number, min?: number) { + return (target: any, propertyKey: string) => { + addValidationRule(target, propertyKey, new StringLengthValidationRule(max, min)); + } +} + + +export function addValidationRule(target: any, propertyKey: string, rule: IValidationRule) { + const rules: IValidationRule[] = Reflect.getMetadata("validation", target, propertyKey) || []; + rules.push(rule); + + const properties: string[] = Reflect.getMetadata("validation", target) || []; + if (properties.indexOf(propertyKey) < 0) { + properties.push(propertyKey); + } + + Reflect.defineMetadata("validation", properties, target); + Reflect.defineMetadata("validation", rules, target, propertyKey); +} + + +export function validate(target: any) { + // Get the list of properties to validate + const keys = Reflect.getMetadata("validation", target) as string[]; + const errorMessages: string[] = []; + if (Array.isArray(keys)) { + for (const key of keys) { + const rules = Reflect.getMetadata("validation", target, key) as IValidationRule[]; + if (!Array.isArray(rules)) { + continue; + } + + for (const rule of rules) { + const error = rule.evaluate(target, target[key], key); + if (error) { + errorMessages.push(error); + } + } + } + } + + return errorMessages; +} + +export function isValid(target: any) { + const validationResult = validate(target); + return validationResult.length === 0; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/assets/.gitkeep b/src/Server/Coderr.Server.WebSite/ClientApp/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/assets/Spinner-1s-200px.svg b/src/Server/Coderr.Server.WebSite/ClientApp/src/assets/Spinner-1s-200px.svg new file mode 100644 index 00000000..b39b3776 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/assets/Spinner-1s-200px.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/environments/environment.prod.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/environments/environment.prod.ts new file mode 100644 index 00000000..10e4ccab --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/environments/environment.prod.ts @@ -0,0 +1,4 @@ +export const environment = { + production: true, + apiUrl: '/' +}; diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/environments/environment.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/environments/environment.ts new file mode 100644 index 00000000..161ccc99 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false, + apiUrl: 'http://localhost:54250/' +}; + +/* + * In development mode, to ignore zone related error stack frames such as + * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can + * import the following file, but please comment it out in production mode + * because it will have performance impact when throw error + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/index.html b/src/Server/Coderr.Server.WebSite/ClientApp/src/index.html new file mode 100644 index 00000000..ea88ff61 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/index.html @@ -0,0 +1,73 @@ + + + + + + + + + Coderr + + + + + + + + + + + +
+ Loading gif +
+
+ + + + + +
+

+
+ + +
+ + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/karma.conf.js b/src/Server/Coderr.Server.WebSite/ClientApp/src/karma.conf.js new file mode 100644 index 00000000..4a9730b9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/karma.conf.js @@ -0,0 +1,31 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + coverageIstanbulReporter: { + dir: require('path').join(__dirname, '../coverage'), + reports: ['html', 'lcovonly'], + fixWebpackSourcePaths: true + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/main.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/main.ts new file mode 100644 index 00000000..a2f708cb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/main.ts @@ -0,0 +1,20 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +export function getBaseUrl() { + return document.getElementsByTagName('base')[0].href; +} + +const providers = [ + { provide: 'BASE_URL', useFactory: getBaseUrl, deps: [] } +]; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic(providers).bootstrapModule(AppModule) + .catch(err => console.log(err)); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/ago.pipe.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/ago.pipe.ts new file mode 100644 index 00000000..440c33a4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/ago.pipe.ts @@ -0,0 +1,72 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/* + * Display either a moment (.i.e. 5 days ago) or a specific date + * depending on the amount of time that have passed since the date. + * + * Usage: + * value | moment:format:default + * Example: + * {{ '2020-01-10 13:00' | moment:'date' }} + * formats to date only +*/ +@Pipe({ name: 'ago' }) +export class AgoPipe implements PipeTransform { + transform(value: string | Date | null, defaultValue?: string, format?: string): string { + if (value === null || typeof value === "undefined") { + return defaultValue || "never"; + } + + let date = null; + if (typeof value === "string") { + + // assume UTC if not specified. + if (value.indexOf('+') === -1 && value.substr(-1, 1) !== 'Z') { + value += 'Z'; + } + + date = new Date(value); + } else { + date = value; + } + + const now = new Date(); + const diffSeconds = (now.getTime() - date.getTime()) / 1000; + if (diffSeconds < 60) { + return "a moment ago"; + } + + const diffMinutes = diffSeconds / 60; + if (diffMinutes < 90) { + if (diffMinutes === 1) { + return "a minute ago"; + } + return Math.round(diffMinutes) + " minutes ago"; + } + + const diffHours = diffSeconds / 60 / 60; + if (diffHours < 20) { + if (diffHours === 1) { + return "an hour ago"; + } + + return Math.round(diffHours) + " hours ago"; + } + + const diffDays = diffSeconds / 60 / 60 / 24; + if (diffDays < 6) { + if (diffDays === 1) { + return "a day ago"; + } + + return Math.round(diffDays) + " days ago"; + } + + if (format === 'full') { + return date.toLocaleString(); + } else { + return date.toLocaleDateString(); + } + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/iso-date.pipe.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/iso-date.pipe.ts new file mode 100644 index 00000000..0aa14c4d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/iso-date.pipe.ts @@ -0,0 +1,39 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +/* + * Display either a moment (.i.e. 5 days ago) or a specific date + * depending on the amount of time that have passed since the date. + * + * Usage: + * value | moment:format:default + * Example: + * {{ '2020-01-10 13:00' | moment:'date' }} + * formats to date only +*/ +@Pipe({ name: 'isoDate' }) +export class IsoDatePipe implements PipeTransform { + transform(value: string | Date | null, format?: string): string { + if (value === null || typeof value === "undefined") { + return "n/a"; + } + let date = null; + if (typeof value === "string") { + + // assume UTC if not specified. + if (value.indexOf('+') === -1 && value.substr(-1, 1) !== 'Z') { + value += 'Z'; + } + + date = new Date(value); + } else { + date = value; + } + + if (format !== 'date') { + return date.toLocaleString(); + } else { + return date.toLocaleDateString(); + } + + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/pipe.module.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/pipe.module.ts new file mode 100644 index 00000000..95f8cd4e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/pipes/pipe.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AgoPipe } from './ago.pipe'; +import { IsoDatePipe } from './iso-date.pipe'; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [ + AgoPipe, + IsoDatePipe + ], + exports: [ + AgoPipe, + IsoDatePipe + ] +}) +export class PipeModule { } diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/polyfills.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/polyfills.ts new file mode 100644 index 00000000..e484510f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/polyfills.ts @@ -0,0 +1,63 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags.ts'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Azure/DevOps.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Azure/DevOps.ts new file mode 100644 index 00000000..39093ac6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Azure/DevOps.ts @@ -0,0 +1,75 @@ +export class SaveAzureSettings { + public static TYPE_NAME: string = 'SaveAzureSettings'; + public PersonalAccessToken: string; + public Url: string; + public ApplicationId: number; + public ProjectName: string; + public ProjectId: string; + public AreaPath: string; + public AreaPathId: string; +} + +export class GetAzureSettings { + public static TYPE_NAME: string = 'GetAzureSettings'; + public ApplicationId: number; +} + + +export class GetAzureSettingsResult { + public PersonalAccessToken: string; + public Url: string; + public ApplicationId: number; + public ProjectName: string; + public ProjectId: string; + public AreaPath: string; + public AreaPathId: string; +} + +export class GetAreaPaths { + public static TYPE_NAME: string = 'GetAreaPaths'; + public PersonalAccessToken: string; + public Url: string; + public ProjectNameOrId: string; +} +export class GetAreaPathsResult { + public Items: GetAreaPathsResultItem[]; +} + +export class GetAreaPathsResultItem { + public Path: string; + public Name: string; + public Id: string; +} + + +export class GetIterations { + public static TYPE_NAME: string = 'GetIterations'; + public PersonalAccessToken: string; + public Url: string; + public ProjectNameOrId: string; +} +export class GetIterationPathsResult { + public Items: GetProjectsResultItem[]; +} + +export class GetIterationPathsResultItem { + public Name: string; + public Id: string; +} + + +export class GetProjects { + public static TYPE_NAME: string = 'GetProjects'; + public PersonalAccessToken: string; + public Url: string; +} + +export class GetProjectsResult { + public Items: GetProjectsResultItem[]; +} + +export class GetProjectsResultItem { + public Name: string; + public Id: string; +} + diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Demo.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Demo.ts new file mode 100644 index 00000000..803a502f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Demo.ts @@ -0,0 +1,17 @@ +export class GetDemoIncidentOptions { + public static TYPE_NAME: string = 'GetDemoIncidentOptions'; +} +export class GetDemoIncidentOptionsResult { + public items: GetDemoIncidentOptionsResultItem[]; +} +export class GetDemoIncidentOptionsResultItem { + public category: string; + public description: string; + public id: string; + public title: string; +} + +export class GenerateDemoIncidents { + public static TYPE_NAME: string = 'GenerateDemoIncidents'; + public demoOptionIds: string[]; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Insights.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Insights.ts new file mode 100644 index 00000000..db1217b1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Insights.ts @@ -0,0 +1,45 @@ +export class GetInsights { + public static TYPE_NAME: string = 'GetInsights'; + public ApplicationId: number | null; +} +export class GetInsightsResult { + public static TYPE_NAME: string = 'GetInsightsResult'; + public ApplicationInsights: GetInsightResultApplication[]; + public Indicators: GetInsightResultIndicator[]; + public TrendDates: string[]; +} +export class GetInsightResultApplication { + public Id: number; + public Name: string; + public NumberOfDevelopers: number; + public Indicators: GetInsightResultIndicator[]; +} +export class GetInsightResultIndicator { + public CanBeNormalized: boolean; + public PeriodValueName: string; + public Name: string; + public Title: string; + public IsAlternative: boolean; + public Description: string; + public Comment: string; + public Value: any; + public ValueName: string; + public PeriodValue: any; + public TrendLines: TrendLine[]; + public Toplist: ToplistItem[]; + public HigherValueIsBetter: boolean | any; + public ValueUnit: null|"days"|"users"|"incidents"|"versions"; +} +export class TrendLine { + public DisplayName: string; + public TrendValues: any[]; +} +export class TrendValue { + public Value: any; + public Normalized: any; +} +export class ToplistItem { + public Title: string; + public Value: string; + public Comment: string; +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Logs.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Logs.ts new file mode 100644 index 00000000..8d77dbf5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Logs.ts @@ -0,0 +1,23 @@ +export class HasLogs { + public static TYPE_NAME: string = 'HasLogs'; + public incidentId: number; + public reportId: number | null; +} +export class HasLogsResult { + public hasLogs: boolean; +} + +export class GetLogs { + public static TYPE_NAME: string = 'GetLogs'; + public incidentId: number; + public reportId: number | null; +} +export class GetLogsResult { + public entries: GetLogsResultEntry[]; +} +export class GetLogsResultEntry { + public message: string; + public level: number; + public timeStampUtc: Date; + public exception: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Mine.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Mine.ts new file mode 100644 index 00000000..db973be6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Mine.ts @@ -0,0 +1,35 @@ +export class ListMyIncidents { + public static TYPE_NAME: string = 'ListMyIncidents'; + public applicationId: number | null; +} +export class ListMyIncidentsResult { + public static TYPE_NAME: string = 'ListMyIncidentsResult'; + public comment: string; + public items: ListMyIncidentsResultItem[]; + public suggestions: ListMySuggestedItem[]; +} +export class ListMyIncidentsResultItem { + public static TYPE_NAME: string = 'ListMyIncidentsResultItem'; + public applicationId: number; + public applicationName: string; + public assignedAtUtc: Date; + public createdAtUtc: Date; + public id: number; + public lastReportAtUtc: Date; + public name: string; + public reportCount: number; +} +export class ListMySuggestedItem { + public static TYPE_NAME: string = 'ListMySuggestedItem'; + public applicationId: number; + public applicationName: string; + public createdAtUtc: Date; + public exceptionTypeName: string; + public id: number; + public lastReportAtUtc: Date; + public name: string; + public weight: number; + public reportCount: number; + public stackTrace: string; + public motivation: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Partitions.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Partitions.ts new file mode 100644 index 00000000..17bdbb2e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/Partitions.ts @@ -0,0 +1,103 @@ + +export class CreatePartition { + public static TYPE_NAME: string = 'CreatePartition'; + applicationId: number; + name: string; + partitionKey: string; + numberOfItems: number; + weight: number; + importantThreshold?: number; + criticalThreshold?: number; +} +export class UpdatePartition { + public static TYPE_NAME: string = 'UpdatePartition'; + id: number; + name: string; + numberOfItems: number; + weight: number; + importantThreshold?: number; + criticalThreshold?: number; +} +export class DeletePartition { + public static TYPE_NAME: string = 'DeletePartition'; + id: number; +} + +export class GetPartitions { + public static TYPE_NAME: string = 'GetPartitions'; + applicationId: number; +} + +export class GetPartitionsResult { + items: GetPartitionsResultItem[]; +} + +export class GetPartitionsResultItem { + id: number; + applicationId: number; + name: string; + partitionKey: string; + weight: number; +} + +export class GetPartition { + public static TYPE_NAME: string = 'GetPartition'; + id: number; +} + +export class GetPartitionResult { + id: number; + applicationId: number; + name: string; + partitionKey: string; + numberOfItems: number; + weight: number; + importantThreshold?: number; + criticalThreshold?: number; +} + +export class GetPartitionValues { + public static TYPE_NAME: string = 'GetPartitionValues'; + partitionId: number; + incidentId?: number; + pageNumber?: number; + pageSize: number = 20; +} + +export class GetPartitionValuesResult { + items: GetPartitionValuesResultItem[]; +} + +export class GetPartitionValuesResultItem { + id: number; + value: number; + receivedAtUtc: Date; +} + +export class GetPartitionInsights { + public static TYPE_NAME: string = 'GetPartitionInsights'; + incidentId?: number; + applicationIds: number[]; + startDate: Date; + endDate: Date; + summarizePeriodStartDate: Date; + summarizePeriodEndDate: Date; +} + +export class GetPartitionInsightsResult { + applications: GetPartitionInsightsResultApplication[]; +} + +export class GetPartitionInsightsResultApplication { + applicationId: number; + indicators: GetPartitionInsightsResultIndicator[]; +} + +export class GetPartitionInsightsResultIndicator { + name: string; + displayName: string; + value: number; + periodValue: number; + dates: string[]; + values: string[]; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/WorkItems.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/WorkItems.ts new file mode 100644 index 00000000..dcbe7cd9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Common/WorkItems.ts @@ -0,0 +1,28 @@ +export class FindIntegration { + public static TYPE_NAME: string = 'FindIntegration'; + public ApplicationId: number; +} + +export class FindIntegrationResult { + public HaveIntegration: boolean; + public Name: string; + public Title: string; +} + +export class FindWorkItem { + public static TYPE_NAME: string = 'FindWorkItem'; + public IncidentId: number; +} + +export class FindWorkItemResult { + public WorkItemId: string; + public Name: string; + public Url: string; + public ApplicationId: number; +} + +export class CreateWorkItem { + public static TYPE_NAME: string = 'CreateWorkItem'; + public IncidentId: number; + public ApplicationId: number; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Accounts.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Accounts.ts new file mode 100644 index 00000000..0b54395b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Accounts.ts @@ -0,0 +1,118 @@ +// ReSharper disable InconsistentNaming +export class RegisterSimple { + public static TYPE_NAME: string = 'RegisterSimple'; + public emailAddress: string; +} +export class AcceptInvitation { + public static TYPE_NAME: string = 'AcceptInvitation'; + public acceptedEmail: string; + public accountId: number; + public emailUsedForTheInvitation: string; + public firstName: string; + public invitationKey: string; + public lastName: string; + public password: string; + public userName: string; +} +export class ChangePassword { + public static TYPE_NAME: string = 'ChangePassword'; + public currentPassword: string; + public newPassword: string; +} +export class ValidateNewLoginReply { + public static TYPE_NAME: string = 'ValidateNewLoginReply'; + public emailIsTaken: boolean; + public userNameIsTaken: boolean; +} +export class AccountDTO { + public static TYPE_NAME: string = 'AccountDTO'; + public createdAtUtc: Date; + public email: string; + public id: number; + public lastLoginAtUtc: Date; + public state: AccountState; + public updatedAtUtc: Date; + public userName: string; +} +export enum AccountState { + VerificationRequired = 0, + Active = 1, + Locked = 2, + ResetPassword = 3, +} +export class FindAccountByUserName { + public static TYPE_NAME: string = 'FindAccountByUserName'; + public userName: string; +} +export class FindAccountByUserNameResult { + public static TYPE_NAME: string = 'FindAccountByUserNameResult'; + public accountId: number; + public displayName: string; +} +export class GetAccountById { + public static TYPE_NAME: string = 'GetAccountById'; + public accountId: number; +} +export class GetAccountEmailById { + public static TYPE_NAME: string = 'GetAccountEmailById'; + public accountId: number; +} +export class AccountActivated { + public static TYPE_NAME: string = 'AccountActivated'; + public accountId: number; + public emailAddress: string; + public userName: string; +} +export class AccountRegistered { + public static TYPE_NAME: string = 'AccountRegistered'; + public accountId: number; + public isSysAdmin: boolean; + public userName: string; +} +export class InvitationAccepted { + public static TYPE_NAME: string = 'InvitationAccepted'; + public acceptedEmailAddress: string; + public accountId: number; + public applicationIds: number[]; + public invitedByUserName: string; + public invitedEmailAddress: string; + public userName: string; +} +export class LoginFailed { + public static TYPE_NAME: string = 'LoginFailed'; + public invalidLogin: boolean; + public isActivated: boolean; + public isLocked: boolean; + public userName: string; +} +export class DeclineInvitation { + public static TYPE_NAME: string = 'DeclineInvitation'; + public invitationId: string; +} +export class RegisterAccount { + public static TYPE_NAME: string = 'RegisterAccount'; + public accountId: number; + public activateDirectly: boolean; + public email: string; + public password: string; + public userName: string; +} +export class RequestPasswordReset { + public static TYPE_NAME: string = 'RequestPasswordReset'; + public emailAddress: string; +} +export class ListAccounts { + public static TYPE_NAME: string = 'ListAccounts'; + +} + +export class ListAccountsResult { + public accounts: ListAccountsResultItem[]; +} + +export class ListAccountsResultItem { + public accountId: number; + public userName: string; + public email: string; + public createdAtUtc: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/ApiKeys.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/ApiKeys.ts new file mode 100644 index 00000000..0f7f6401 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/ApiKeys.ts @@ -0,0 +1,65 @@ +// ReSharper disable InconsistentNaming + +export class GetApiKey { + public static TYPE_NAME: string = 'GetApiKey'; + public apiKey: string; + public id: number; +} +export class GetApiKeyResult { + public static TYPE_NAME: string = 'GetApiKeyResult'; + public allowedApplications: GetApiKeyResultApplication[]; + public applicationName: string; + public createdAtUtc: Date; + public createdById: number; + public generatedKey: string; + public id: number; + public sharedSecret: string; +} +export class GetApiKeyResultApplication { + public static TYPE_NAME: string = 'GetApiKeyResultApplication'; + public applicationId: number; + public applicationName: string; +} +export class ListApiKeys { + public static TYPE_NAME: string = 'ListApiKeys'; +} +export class ListApiKeysResult { + public static TYPE_NAME: string = 'ListApiKeysResult'; + public keys: ListApiKeysResultItem[]; +} +export class ListApiKeysResultItem { + public static TYPE_NAME: string = 'ListApiKeysResultItem'; + public apiKey: string; + public applicationName: string; + public id: number; +} +export class ApiKeyCreated { + public static TYPE_NAME: string = 'ApiKeyCreated'; + public apiKey: string; + public applicationIds: number[]; + public applicationNameForTheAppUsingTheKey: string; + public createdById: number; + public sharedSecret: string; +} +export class ApiKeyRemoved { + public static TYPE_NAME: string = 'ApiKeyRemoved'; +} +export class CreateApiKey { + public static TYPE_NAME: string = 'CreateApiKey'; + public accountId: number; + public apiKey: string; + public applicationIds: number[]; + public applicationName: string; + public sharedSecret: string; +} +export class DeleteApiKey { + public static TYPE_NAME: string = 'DeleteApiKey'; + public apiKey: string; + public id: number; +} +export class EditApiKey { + public static TYPE_NAME: string = 'EditApiKey'; + public applicationIds: number[]; + public applicationName: string; + public id: number; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Applications.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Applications.ts new file mode 100644 index 00000000..5c97f0f7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Applications.ts @@ -0,0 +1,239 @@ +export class ApplicationListItem { + public static TYPE_NAME: string = 'ApplicationListItem'; + public id: number; + public name: string; + public isAdmin: boolean; + public retentionDays: number; +} +export enum TypeOfApplication { + Mobile = 0, + DesktopApplication = 1, + Server = 2, +} +export class GetApplicationIdByKey { + public static TYPE_NAME: string = 'GetApplicationIdByKey'; + public applicationKey: string; +} +export class GetApplicationIdByKeyResult { + public static TYPE_NAME: string = 'GetApplicationIdByKeyResult'; + public id: number; +} +export class GetApplicationInfo { + public static TYPE_NAME: string = 'GetApplicationInfo'; + public appKey: string; + public applicationId: number; +} +export class GetApplicationInfoResult { + public static TYPE_NAME: string = 'GetApplicationInfoResult'; + public appKey: string; + public applicationType: TypeOfApplication; + public id: number; + public name: string; + public sharedSecret: string; + public totalIncidentCount: number; + public lastIncidentAtUtc: Date; + public numberOfDevelopers: number; + public versions: string[]; + public showStatsQuestion: boolean; + public retentionDays: number; +} +export class GetApplicationList { + public static TYPE_NAME: string = 'GetApplicationList'; + public accountId: number; + public filterAsAdmin: boolean; +} +export class GetApplicationOverview { + public static TYPE_NAME: string = 'GetApplicationOverview'; + public applicationId: number; + public numberOfDays: number; + public version: string; + public includeChartData: boolean = true; + public includePartitions: boolean; +} +export class GetApplicationOverviewResult { + public static TYPE_NAME: string = 'GetApplicationOverviewResult'; + public days: number; + public errorReports: number[]; + public incidents: number[]; + public statSummary: OverviewStatSummary; + public timeAxisLabels: string[]; +} +export class GetApplicationTeam { + public static TYPE_NAME: string = 'GetApplicationTeam'; + public applicationId: number; +} +export class GetApplicationTeamMember { + public static TYPE_NAME: string = 'GetApplicationTeamMember'; + public joinedAtUtc: Date; + public userId: number; + public userName: string; + public isAdmin: boolean; +} +export class GetApplicationTeamResult { + public static TYPE_NAME: string = 'GetApplicationTeamResult'; + public invited: GetApplicationTeamResultInvitation[]; + public members: GetApplicationTeamMember[]; +} +export class GetApplicationTeamResultInvitation { + public static TYPE_NAME: string = 'GetApplicationTeamResultInvitation'; + public emailAddress: string; + public invitedAtUtc: Date; + public invitedByUserName: string; +} +export class OverviewStatSummary { + public followers: number; + public incidents: number; + public newestIncidentReceivedAtUtc?: string; + public reports: number; + public newestReportReceivedAtUtc?: string; + public userFeedback: number; + public partitions: PartitionOverview[]; +} +export class PartitionOverview { + public name: string; + public displayName: string; + public value: number; +} +export class ApplicationCreated { + public static TYPE_NAME: string = 'ApplicationCreated'; + public appKey: string; + public applicationId: number; + public applicationName: string; + public createdById: number; + public sharedSecret: string; +} +export class ApplicationDeleted { + public static TYPE_NAME: string = 'ApplicationDeleted'; + public appKey: string; + public applicationId: number; + public applicationName: string; +} +export class UserAddedToApplication { + public static TYPE_NAME: string = 'UserAddedToApplication'; + public accountId: number; + public applicationId: number; +} +export class UserInvitedToApplication { + public static TYPE_NAME: string = 'UserInvitedToApplication'; + public applicationId: number; + public applicationName: string; + public emailAddress: string; + public invitationKey: string; + public invitedBy: string; +} +export class CreateApplication { + public static TYPE_NAME: string = 'CreateApplication'; + public groupId?: number; + public applicationKey: string; + public name: string; + public typeOfApplication: TypeOfApplication; + public numberOfDevelopers?: number; + public numberOfErrors?: number; + public retentionDays?: number; +} +export class DeleteApplication { + public static TYPE_NAME: string = 'DeleteApplication'; + public id: number; +} +export class RemoveTeamMember { + public static TYPE_NAME: string = 'RemoveTeamMember'; + public applicationId: number; + public userToRemove: number; +} +export class UpdateApplication { + public static TYPE_NAME: string = 'UpdateApplication'; + public applicationId: number; + public name: string; + public typeOfApplication: TypeOfApplication | null; + public retentionDays?: number; +} +export class AddTeamMember { + public static TYPE_NAME: string = 'AddTeamMember'; + public applicationId: number; + public userToAdd: number; + public roles: string[]; +} +export class UpdateRoles { + public static TYPE_NAME: string = 'UpdateRoles'; + public applicationId: number; + public userToUpdate: number; + public roles: string[]; +} + + +export class GetApplicationVersions { + public static TYPE_NAME: string = 'GetApplicationVersions'; + public applicationId: number; +} + +export class GetApplicationVersionsResult { + public items: GetApplicationVersionsResultItem[]; +} + +export class GetApplicationVersionsResultItem { + public firstReportReceivedAtUtc: Date; + public incidentCount: number; + public lastReportReceivedAtUtc: Date; + public reportCount: number; + public version: string; +} + +export class GetApplicationGroups { + public static TYPE_NAME: string = 'GetApplicationGroups'; +} + +export class GetApplicationGroupsResult { + public items: GetApplicationGroupsResultItem[]; +} + +export class GetApplicationGroupsResultItem { + public id: number; + public name: string; +} + +export class MapApplicationsToGroup { + public static TYPE_NAME: string = "MapApplicationsToGroup"; + public groupId: number; + public applicationIds: number[]; +} + +export class RenameApplicationGroup { + public static TYPE_NAME: string = "RenameApplicationGroup"; + public groupId: number; + public newName: string; +} + +export class GetApplicationGroupMap { + public static TYPE_NAME: string = 'GetApplicationGroupMap'; + public applicationId?: number; +} + +export class GetApplicationGroupMapResult { + public items: GetApplicationGroupMapResultItem[]; +} + + +export class GetApplicationGroupMapResultItem { + public applicationId: number; + public groupId: number; +} + +export class CreateApplicationGroup { + public static TYPE_NAME: string = 'CreateApplicationGroup'; + public name: string; +} + +export class SetApplicationGroup { + public static TYPE_NAME: string = 'SetApplicationGroup'; + public applicationId: number; + public applicationGroupId: number; + public appKey: string; + public groupName: string; +} + + +export class DeleteApplicationGroup { + public static TYPE_NAME: string = 'DeleteApplicationGroup'; + public groupId: number; + public moveAppsToGroupId: number; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Environments.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Environments.ts new file mode 100644 index 00000000..3727e705 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Environments.ts @@ -0,0 +1,32 @@ +export class GetEnvironments { + public static TYPE_NAME: string = 'GetEnvironments'; + public applicationId: number; +} +export class GetEnvironmentsResult { + public static TYPE_NAME: string = 'GetEnvironments'; + public items: GetEnvironmentsResultItem[]; +} +export class GetEnvironmentsResultItem { + public static TYPE_NAME: string = 'GetEnvironmentsResultItem'; + public id: number; + public name: string; + public deleteIncidents: boolean; +} +export class ResetEnvironment { + public static TYPE_NAME: string = 'ResetEnvironment'; + public environmentId: number; + public applicationId: number; +} +export class UpdateEnvironment { + public static TYPE_NAME: string = 'UpdateEnvironment'; + public environmentId: number; + public applicationId: number; + public deleteIncidents: boolean; +} + +export class CreateEnvironment { + public static TYPE_NAME: string = 'CreateEnvironment'; + public applicationId: number; + public name: string; + public deleteIncidents: boolean; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Feedback.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Feedback.ts new file mode 100644 index 00000000..9e926fc5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Feedback.ts @@ -0,0 +1,15 @@ +export class FeedbackAttachedToIncident { + public static TYPE_NAME: string = 'FeedbackAttachedToIncident'; + public incidentId: number; + public message: string; + public userEmailAddress: string; +} +export class SubmitFeedback { + public static TYPE_NAME: string = 'SubmitFeedback'; + public createdAtUtc: Date; + public email: string; + public errorId: string; + public feedback: string; + public remoteAddress: string; + public reportId: number; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Incidents.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Incidents.ts new file mode 100644 index 00000000..9ae2f70f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Incidents.ts @@ -0,0 +1,231 @@ +import { ReportDTO } from './Reports' +// ReSharper disable InconsistentNaming + +export enum SortOrder { + Newest = 0, + LatestReport = 1, + ReportCount = 2, +} +export class IncidentSummaryDTO { + public static TYPE_NAME: string = 'IncidentSummaryDTO'; + public applicationId: number; + public applicationName: string; + public createdAtUtc: Date; + public id: number; + public isReOpened: boolean; + public assignedToUserId: number | null; + public lastUpdateAtUtc: Date; + public name: string; + public reportCount: number; +} +export class FindIncidents { + public static TYPE_NAME: string = 'FindIncidents'; + public applicationIds: number[]; + public freeText: string; + public isAssigned: boolean; + public isClosed: boolean; + public isIgnored: boolean; + public isNew: boolean; + public itemsPerPage: number; + public maxDate: Date; + public minDate: Date; + public pageNumber: number; + public reOpened: boolean; + public sortAscending: boolean; + public sortType: SortOrder; + public version: string; + public tags: string[]; + public assignedToId: number; + public contextCollectionName: string; + public contextCollectionPropertyName: string; + public contextCollectionPropertyValue: string; + public environmentIds: number[]; +} +export class FindIncidentsResult { + public static TYPE_NAME: string = 'FindIncidentsResult'; + public items: FindIncidentsResultItem[]; + public pageNumber: number; + public pageSize: number; + public totalCount: number; +} +export class FindIncidentsResultItem { + public static TYPE_NAME: string = 'FindIncidentsResultItem'; + public applicationId: number; + public applicationName: string; + public assignedAtUtc: Date | null; + public createdAtUtc: Date; + public id: number; + public isReOpened: boolean; + public lastUpdateAtUtc: Date; + public name: string; + public reportCount: number; + public lastReportReceivedAtUtc: Date; +} +export class GetIncident { + public static TYPE_NAME: string = 'GetIncident'; + public incidentId: number; +} +export class GetIncidentForClosePage { + public static TYPE_NAME: string = 'GetIncidentForClosePage'; + public incidentId: number; +} +export class GetIncidentForClosePageResult { + public static TYPE_NAME: string = 'GetIncidentForClosePageResult'; + public description: string; + public subscriberCount: number; +} +export class GetIncidentResult { + public static TYPE_NAME: string = 'GetIncidentResult'; + public applicationId: number; + public assignedAtUtc: Date | null; + public assignedTo: string; + public assignedToId: number | null; + public contextCollections: string[]; + public createdAtUtc: Date; + public dayStatistics: ReportDay[]; + public description: string; + public facts: QuickFact[]; + public fullName: string; + public hashCodeIdentifier: string; + public id: number; + public incidentState: number; + public isIgnored: boolean; + public isReOpened: boolean; + public isSolutionShared: boolean; + public isSolved: boolean; + public lastReportReceivedAtUtc: Date; + public previousSolutionAtUtc: Date; + public reOpenedAtUtc: Date; + public reportCount: number; + public reportHashCode: string; + public solution: string; + public solvedAtUtc: Date; + public stackTrace: string; + public tags: string[]; + public updatedAtUtc: Date; + public suggestedSolutions: SuggestedIncidentSolution[]; + public highlightedContextData: HighlightedContextData[]; +} +export class GetIncidentStatistics { + public static TYPE_NAME: string = 'GetIncidentStatistics'; + public incidentId: number; + public numberOfDays: number; +} +export class GetIncidentStatisticsResult { + public static TYPE_NAME: string = 'GetIncidentStatisticsResult'; + public labels: string[]; + public values: number[]; +} +export class HighlightedContextData { + public static TYPE_NAME: string = 'HighlightedContextData'; + public description: string; + public name: string; + public url: string; + public value: string[]; +} +export class QuickFact { + public static TYPE_NAME: string = 'QuickFact'; + public description: string; + public title: string; + public url: string; + public value: string; +} +export class ReportDay { + public static TYPE_NAME: string = 'ReportDay'; + public count: number; + public date: string; +} +export class SuggestedIncidentSolution { + public static TYPE_NAME: string = 'SuggestedIncidentSolution'; + public reason: string; + public suggestedSolution: string; +} +export class IncidentCreated { + public static TYPE_NAME: string = 'IncidentCreated'; + public applicationId: number; + public applicationVersion: string; + public createdAtUtc: string; + public exceptionTypeName: string; + public incidentId: number; + public incidentName: string; +} +export class IncidentAssigned { + public static TYPE_NAME: string = 'IncidentAssigned'; + public assignedById: number; + public assignedToId: number; + public incidentId: number; +} +export class IncidentIgnored { + public static TYPE_NAME: string = 'IncidentIgnored'; + applicationId: number; + public accountId: number; + public incidentId: number; + public userName: string; +} +export class IncidentReOpened { + public static TYPE_NAME: string = 'IncidentReOpened'; + public applicationId: number; + public createdAtUtc: Date; + public incidentId: number; +} +export class IncidentClosed { + public static TYPE_NAME: string = 'IncidentClosed'; + applicationId: number; + public closedById: number; + public incidentId: number; + public solution: string; + public applicationVersion: string; + public closedAtUtc: Date; +} +export class IncidentEscalated { + public static TYPE_NAME: string = 'IncidentEscalated'; + public applicationId: number; + public incidentId: number; + public isCritical: boolean; + public isImportant: boolean; +} +export class ReportAddedToIncident { + public static TYPE_NAME: string = 'ReportAddedToIncident'; + public incident: IncidentSummaryDTO; + public isReOpened: boolean; + public report: ReportDTO; +} +export class AssignIncident { + public static TYPE_NAME: string = 'AssignIncident'; + public assignedBy: number; + public assignedTo: number; + public incidentId: number; +} +export class CloseIncident { + public static TYPE_NAME: string = 'CloseIncident'; + public canSendNotification: boolean; + public incidentId: number; + public notificationText: string; + public notificationTitle: string; + public shareSolution: boolean; + public solution: string; + public userId: number; + public applicationVersion: string; +} +export class DeleteIncident { + public static TYPE_NAME: string = 'DeleteIncident'; + public areYouSure: string; + public incidentId: number; +} +export class IgnoreIncident { + public static TYPE_NAME: string = 'IgnoreIncident'; + public incidentId: number; + public userId: number; +} +export class ReOpenIncident { + public static TYPE_NAME: string = 'ReOpenIncident'; + public incidentId: number; + public userId: number; +} + +export class NotifySubscribers { + public static TYPE_NAME: string = 'NotifySubscribers'; + public incidentId: number; + public body: string; + public title: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Invitations.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Invitations.ts new file mode 100644 index 00000000..fd1a3e51 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Invitations.ts @@ -0,0 +1,19 @@ +export class GetInvitationByKey { + public static TYPE_NAME: string = 'GetInvitationByKey'; + public invitationKey: string; +} +export class GetInvitationByKeyResult { + public static TYPE_NAME: string = 'GetInvitationByKeyResult'; + public emailAddress: string; +} +export class InviteUser { + public static TYPE_NAME: string = 'InviteUser'; + public applicationId: number; + public emailAddress: string; + public text: string; +} +export class DeleteInvitation { + public static TYPE_NAME: string = 'DeleteInvitation'; + public applicationId: number; + public invitedEmailAddress: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Messaging.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Messaging.ts new file mode 100644 index 00000000..7b5a9a64 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Messaging.ts @@ -0,0 +1,28 @@ +export class EmailAddress { + public static TYPE_NAME: string = 'EmailAddress'; + public address: string; + public name: string; +} +export class EmailMessage { + public static TYPE_NAME: string = 'EmailMessage'; + public htmlBody: string; + public recipients: EmailAddress[]; + public replyTo: EmailAddress; + public resources: EmailResource[]; + public subject: string; + public textBody: string; +} +export class EmailResource { + public static TYPE_NAME: string = 'EmailResource'; + public content: string /*base64 encoded data*/; + public name: string; +} +export class SendEmail { + public static TYPE_NAME: string = 'SendEmail'; + public emailMessage: EmailMessage; +} +export class SendSms { + public static TYPE_NAME: string = 'SendSms'; + public message: string; + public phoneNumber: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Notifications.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Notifications.ts new file mode 100644 index 00000000..b1023df6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Notifications.ts @@ -0,0 +1,8 @@ +export class AddNotification { + public static TYPE_NAME: string = 'AddNotification'; + public accountId: number | null; + public holdbackInterval: string | null; + public message: string; + public notificationType: string; + public roleName: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Reports.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Reports.ts new file mode 100644 index 00000000..1212e66d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Reports.ts @@ -0,0 +1,89 @@ +export class ContextCollectionDTO { + public static TYPE_NAME: string = 'ContextCollectionDTO'; + public name: string; + public properties: string[]; +} +export class ReportDTO { + public static TYPE_NAME: string = 'ReportDTO'; + public applicationId: number; + public contextCollections: ContextCollectionDTO[]; + public createdAtUtc: Date; + public exception: ReportExeptionDTO; + public id: number; + public incidentId: number; + public remoteAddress: string; + public reportId: string; + public reportVersion: string; +} +export class ReportExeptionDTO { + public static TYPE_NAME: string = 'ReportExeptionDTO'; + public assemblyName: string; + public baseClasses: string[]; + public everything: string; + public fullName: string; + public innerException: ReportExeptionDTO; + public message: string; + public name: string; + public namespace: string; + public properties: string[]; + public stackTrace: string; +} +export class GetReport { + public static TYPE_NAME: string = 'GetReport'; + public reportId: number; +} +export class GetReportException { + public static TYPE_NAME: string = 'GetReportException'; + public assemblyName: string; + public baseClasses: string[]; + public everything: string; + public fullName: string; + public innerException: GetReportException; + public message: string; + public name: string; + public namespace: string; + public stackTrace: string; +} +export class GetReportList { + public static TYPE_NAME: string = 'GetReportList'; + public incidentId: number; + public pageNumber: number; + public pageSize: number; +} +export class GetReportListResult { + public static TYPE_NAME: string = 'GetReportListResult'; + public items: GetReportListResultItem[]; + public pageNumber: number; + public pageSize: number; + public totalCount: number; +} +export class GetReportListResultItem { + public static TYPE_NAME: string = 'GetReportListResultItem'; + public createdAtUtc: string; + public id: number; + public message: string; + public remoteAddress: string; +} +export class GetReportResult { + public static TYPE_NAME: string = 'GetReportResult'; + public contextCollections: GetReportResultContextCollection[]; + public createdAtUtc: string; + public emailAddress: string; + public errorId: string; + public exception: GetReportException; + public id: string; + public incidentId: string; + public message: string; + public stackTrace: string; + public userFeedback: string; +} +export class GetReportResultContextCollection { + public static TYPE_NAME: string = 'GetReportResultContextCollection'; + public name: string; + public properties: KeyValuePair[]; +} +export class KeyValuePair { + public static TYPE_NAME: string = 'KeyValuePair'; + public key: string; + public value: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Support.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Support.ts new file mode 100644 index 00000000..4f595265 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Support.ts @@ -0,0 +1,6 @@ +export class SendSupportRequest { + public static TYPE_NAME: string = 'SendSupportRequest'; + public message: string; + public subject: string; + public url: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Users.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Users.ts new file mode 100644 index 00000000..ac8de2da --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Users.ts @@ -0,0 +1,85 @@ +export class NotificationSettings { + public static TYPE_NAME: string = 'NotificationSettings'; + public notifyOnCriticalIncidents: NotificationState; + public notifyOnImportantIncidents: NotificationState; + public notifyOnNewIncidents: NotificationState; + public notifyOnPeaks: NotificationState; + public notifyOnReOpenedIncident: NotificationState; + public notifyOnUserFeedback: NotificationState; +} +export enum NotificationState { + UseGlobalSetting = 1, + Disabled = 2, + Cellphone = 3, + Email = 4, + BrowserNotification = 5 +} +export class GetUserSettings { + public static TYPE_NAME: string = 'GetUserSettings'; + public applicationId: number; +} +export class GetUserSettingsResult { + public static TYPE_NAME: string = 'GetUserSettingsResult'; + public emailAddress: string; + public firstName: string; + public lastName: string; + public mobileNumber: string; + public notifications: NotificationSettings; +} +export class UpdateNotifications { + public static TYPE_NAME: string = 'UpdateNotifications'; + public applicationId: number; + public notifyOnCriticalIncidents: NotificationState; + public notifyOnImportantIncidents: NotificationState; + public notifyOnNewIncidents: NotificationState; + public notifyOnPeaks: NotificationState; + public notifyOnReOpenedIncident: NotificationState; + public notifyOnUserFeedback: NotificationState; + public userId: number; +} +export class UpdatePersonalSettings { + public static TYPE_NAME: string = 'UpdatePersonalSettings'; + public emailAddress: string; + public firstName: string; + public lastName: string; + public mobileNumber: string; +} + +export class StoreBrowserSubscription { + public static TYPE_NAME: string = 'StoreBrowserSubscription'; + public userId: number | null; + public endpoint?: string; + public expirationTime?: number; + public publicKey: string; + public authenticationSecret: string; +} + +export class DeleteBrowserSubscription { + public static TYPE_NAME: string = 'DeleteBrowserSubscription'; + public userId: number | null; + public endpoint: string; +} +export class GetAccountSetting { + public static TYPE_NAME: string = 'GetAccountSetting'; + public accountId: number; + public name: string; +} +export class GetAccountSettingResult { + public value: string; +} +export class GetAccountSettings { + public static TYPE_NAME: string = 'GetAccountSettings'; + public accountId: number; +} +interface IDictionary { + [key: string]: T; +} +export class GetAccountSettingsResult { + public values: IDictionary; +} +export class SaveAccountSetting { + public static TYPE_NAME: string = 'SaveAccountSetting'; + public accountId: number; + public name: string; + public value: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Whitelist.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Whitelist.ts new file mode 100644 index 00000000..da90308b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Core/Whitelist.ts @@ -0,0 +1,55 @@ +// ReSharper disable InconsistentNaming + +export class GetWhitelistEntries { + public static TYPE_NAME: string = 'GetWhitelistEntries'; + public domainName: string; + public applicationId: number | null; +} +export class GetWhitelistEntriesResult { + public entries: GetWhitelistEntriesResultItem[]; +} +export class GetWhitelistEntriesResultItem { + public id: number; + public applications: GetWhiteListEntriesResultApp[]; + public domainName: string; + public ipAddresses: GetWhiteListEntriesResultIp[]; +} +export class GetWhiteListEntriesResultApp { + public applicationId: number; + public name: string; +} +export class GetWhiteListEntriesResultIp { + public id: number; + public address: string; + public type: IpType; +} +export enum IpType { + Lookup = 0, + Manual = 1, + Denied = 2 +} +export class DomainIpAddress { + public id: number; + public value: string; + public ipType: IpType; +} +export class AddEntry { + public static TYPE_NAME: string = 'AddEntry'; + public applicationIds: number[] | null; + public ipAddresses: string[] | null; + public domainName: string; +} +export class RemoveEntry { + public static TYPE_NAME: string = 'RemoveEntry'; + public id: Number; +} +export class EditEntry { + public static TYPE_NAME: string = 'EditEntry'; + public domainName: string; + public id: Number; + public applicationIds: number[] | null; + + // Contains only manually specified ip addresses. + // denied and lookup addresses do not appear + public ipAddresses: string[] | null; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/ContextData.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/ContextData.ts new file mode 100644 index 00000000..2f96a165 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/ContextData.ts @@ -0,0 +1,24 @@ +export class GetSimilarities { + public static TYPE_NAME: string = 'GetSimilarities'; + public incidentId: number; +} +export class GetSimilaritiesCollection { + public static TYPE_NAME: string = 'GetSimilaritiesCollection'; + public name: string; + public similarities: GetSimilaritiesSimilarity[]; +} +export class GetSimilaritiesResult { + public static TYPE_NAME: string = 'GetSimilaritiesResult'; + public collections: GetSimilaritiesCollection[]; +} +export class GetSimilaritiesSimilarity { + public static TYPE_NAME: string = 'GetSimilaritiesSimilarity'; + public name: string; + public values: GetSimilaritiesValue[]; +} +export class GetSimilaritiesValue { + public static TYPE_NAME: string = 'GetSimilaritiesValue'; + public count: number; + public percentage: number; + public value: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/ErrorOrigins.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/ErrorOrigins.ts new file mode 100644 index 00000000..7366d5ea --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/ErrorOrigins.ts @@ -0,0 +1,14 @@ +export class GetOriginsForIncident { + public static TYPE_NAME: string = 'GetOriginsForIncident'; + public incidentId: number; +} +export class GetOriginsForIncidentResult { + public static TYPE_NAME: string = 'GetOriginsForIncidentResult'; + public items: GetOriginsForIncidentResultItem[]; +} +export class GetOriginsForIncidentResultItem { + public static TYPE_NAME: string = 'GetOriginsForIncidentResultItem'; + public latitude: number; + public longitude: number; + public numberOfErrorReports: number; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/History.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/History.ts new file mode 100644 index 00000000..838a90bc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/History.ts @@ -0,0 +1,29 @@ +export class GetIncidentStateSummary { + static TYPE_NAME: string = "GetIncidentStateSummary"; + applicationId: number; + applicationVersion: string; +} + +export class GetIncidentStateSummaryResult { + reOpenedCount: number; + newCount: number; + closedCount: number; +} + +export class GetIncidentsForStates { + static TYPE_NAME: string = "GetIncidentsForStates"; + applicationId: number; + applicationVersion: string; +} + +export class GetIncidentsForStatesResult { + items: GetIncidentsForStatesResultItem[]; +} +export class GetIncidentsForStatesResultItem { + incidentId: number; + incidentName: string; + createdAtUtc: Date; + isClosed: boolean; + isNew: boolean; + isReopened: boolean; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Onboarding.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Onboarding.ts new file mode 100644 index 00000000..342c3d8f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Onboarding.ts @@ -0,0 +1,19 @@ +export class SetOnboardingChoices +{ + public static TYPE_NAME: string = 'SetOnboardingChoices'; + public Libraries: string[]; + public MainLanguage: string; + public Feedback: string; +} +export class GetOnboardingState { + public static TYPE_NAME: string = 'GetOnboardingState'; + public Libraries: string[]; + public MainLanguage: string; +} +export class GetOnboardingStateResult +{ + public static TYPE_NAME: string = 'GetOnboardingStateResult'; + public IsComplete: boolean; + public Libraries: string[]; + public MainLanguage: string; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Tagging.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Tagging.ts new file mode 100644 index 00000000..90fffd17 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Tagging.ts @@ -0,0 +1,28 @@ +export class TagDTO +{ + public static TYPE_NAME: string = 'TagDTO'; + public name: string; + public orderNumber: number; +} +export class GetTagsForApplication +{ + public static TYPE_NAME: string = 'GetTagsForApplication'; + public aplicationId: number; +} +export class GetTags +{ + public static TYPE_NAME: string = 'GetTags'; + public applicationId?: number; + public incidentId?: number; +} +export class GetTagsForIncident +{ + public static TYPE_NAME: string = 'GetTagsForIncident'; + public incidentId: number; +} +export class TagAttachedToIncident +{ + public static TYPE_NAME: string = 'TagAttachedToIncident'; + public incidentId: number; + public tags: TagDTO[]; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Triggers.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Triggers.ts new file mode 100644 index 00000000..ba25e298 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Triggers.ts @@ -0,0 +1,123 @@ +export enum LastTriggerActionDTO +{ + ExecuteActions = 0, + AbortTrigger = 1, +} +export class TriggerActionDataDTO +{ + public static TYPE_NAME: string = 'TriggerActionDataDTO'; + public ActionContext: string; + public ActionName: string; +} +export class TriggerContextRule +{ + public static TYPE_NAME: string = 'TriggerContextRule'; + public ContextName: string; + public PropertyName: string; + public PropertyValue: string; + public Filter: TriggerFilterCondition; + public ResultToUse: TriggerRuleAction; +} +export class TriggerDTO +{ + public static TYPE_NAME: string = 'TriggerDTO'; + public Description: string; + public Id: string; + public Name: string; + public Summary: string; +} +export class TriggerExceptionRule +{ + public static TYPE_NAME: string = 'TriggerExceptionRule'; + public FieldName: string; + public Value: string; + public Filter: TriggerFilterCondition; + public ResultToUse: TriggerRuleAction; +} +export enum TriggerFilterCondition +{ + StartsWith = 0, + EndsWith = 1, + Contains = 2, + DoNotContain = 3, + Equals = 4, +} +export enum TriggerRuleAction +{ + AbortTrigger = 0, + ContinueWithNextRule = 1, + ExecuteActions = 2, +} +export class TriggerRuleBase +{ + public static TYPE_NAME: string = 'TriggerRuleBase'; + public Filter: TriggerFilterCondition; + public ResultToUse: TriggerRuleAction; +} +export class GetContextCollectionMetadata +{ + public static TYPE_NAME: string = 'GetContextCollectionMetadata'; + public ApplicationId: number; +} +export class GetContextCollectionMetadataItem +{ + public static TYPE_NAME: string = 'GetContextCollectionMetadataItem'; + public Name: string; + public Properties: string[]; +} +export class GetTrigger +{ + public static TYPE_NAME: string = 'GetTrigger'; + public Id: number; +} +export class GetTriggerDTO +{ + public static TYPE_NAME: string = 'GetTriggerDTO'; + public Actions: TriggerActionDataDTO[]; + public ApplicationId: number; + public Description: string; + public Id: number; + public LastTriggerAction: LastTriggerActionDTO; + public Name: string; + public Rules: TriggerRuleBase[]; + public RunForExistingIncidents: boolean; + public RunForNewIncidents: boolean; + public RunForReOpenedIncidents: boolean; +} +export class GetTriggersForApplication +{ + public static TYPE_NAME: string = 'GetTriggersForApplication'; + public ApplicationId: number; +} +export class CreateTrigger +{ + public static TYPE_NAME: string = 'CreateTrigger'; + public Actions: TriggerActionDataDTO[]; + public ApplicationId: number; + public Description: string; + public Id: number; + public LastTriggerAction: LastTriggerActionDTO; + public Name: string; + public Rules: TriggerRuleBase[]; + public RunForExistingIncidents: boolean; + public RunForNewIncidents: boolean; + public RunForReOpenedIncidents: boolean; +} +export class DeleteTrigger +{ + public static TYPE_NAME: string = 'DeleteTrigger'; + public Id: number; +} +export class UpdateTrigger +{ + public static TYPE_NAME: string = 'UpdateTrigger'; + public Actions: TriggerActionDataDTO[]; + public Description: string; + public Id: number; + public LastTriggerAction: LastTriggerActionDTO; + public Name: string; + public Rules: TriggerRuleBase[]; + public RunForExistingIncidents: boolean; + public RunForNewIncidents: boolean; + public RunForReOpenedIncidents: boolean; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Versions.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Versions.ts new file mode 100644 index 00000000..37532a35 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Modules/Versions.ts @@ -0,0 +1,36 @@ +export class GetApplicationVersions +{ + public static TYPE_NAME: string = 'GetApplicationVersions'; + public ApplicationId: number; +} +export class GetApplicationVersionsResult +{ + public static TYPE_NAME: string = 'GetApplicationVersionsResult'; + public Items: GetApplicationVersionsResultItem[]; +} +export class GetApplicationVersionsResultItem +{ + public static TYPE_NAME: string = 'GetApplicationVersionsResultItem'; + public FirstReportReceivedAtUtc: Date; + public IncidentCount: number; + public LastReportReceivedAtUtc: Date; + public ReportCount: number; + public Version: string; +} +export class GetVersionHistory +{ + public static TYPE_NAME: string = 'GetVersionHistory'; + public ApplicationId: number; + public FromDate: Date; + public ToDate: Date; +} +export class GetVersionHistoryResult +{ + public Dates: string[]; + public IncidentCounts: GetVersionHistorySeries[]; + public ReportCounts: GetVersionHistorySeries[]; +} +export class GetVersionHistorySeries { + Name: string; + Values: number[]; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Premise/ActiveDirectory.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Premise/ActiveDirectory.ts new file mode 100644 index 00000000..8fce6614 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Premise/ActiveDirectory.ts @@ -0,0 +1,69 @@ +export class SearchAd { + public static TYPE_NAME: string = 'SearchAd'; + public FindGroups: boolean; + public FindUsers: boolean; + public Text: string; + + public PageNumber: number; + public PageSize: number; +} + +export class SearchAdResult { + public static TYPE_NAME: string = 'SearchAdResult'; + public Items: SearchAdResultItem[]; + public PageNumber: number; + public TotalCount: number; +} + +export class SearchAdResultItem { + public static TYPE_NAME: string = 'SearchAdResultItem'; + public Name: string; + public FullName: string; + public Sid: string; + public Type: string; +} + +export class AddAdGroupToTeam { + public static TYPE_NAME: string = 'AddAdGroupToTeam'; + public ApplicationId: number; + public Sid: string; + public IsAdmin?: boolean; +} + +export class AddAdUserToTeam { + public static TYPE_NAME: string = 'AddAdUserToTeam'; + public ApplicationId: number; + public Sid: string; + public IsAdmin?: boolean; +} + +export class ChangeAdTeamRole { + public static TYPE_NAME: string = 'ChangeAdTeamRole'; + public ApplicationId: number; + public Sid: string; + public IsAdmin: boolean; +} + +export class RemoveAdTeamMember { + public static TYPE_NAME: string = 'RemoveAdTeamMember'; + public ApplicationId: number; + public Sid: string; +} + +export class GetAdTeamMembers { + public static TYPE_NAME: string = 'GetAdTeamMembers'; + public ApplicationId: number; +} + +export class GetAdTeamMembersResult { + public static TYPE_NAME: string = 'GetAdTeamMembersResult'; + public Items: GetAdTeamMembersResultItem[]; +} + +export class GetAdTeamMembersResultItem { + public static TYPE_NAME: string = 'GetAdTeamMembersResultItem'; + public Name: string; + public Sid: string; + public IsGroup: boolean; + public IsAdmin: boolean; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Web/Feedback.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Web/Feedback.ts new file mode 100644 index 00000000..ff49f677 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Web/Feedback.ts @@ -0,0 +1,59 @@ +export class GetFeedbackForApplicationPage +{ + public static TYPE_NAME: string = 'GetFeedbackForApplicationPage'; + public applicationId: number; +} +export class GetFeedbackForApplicationPageResult +{ + public static TYPE_NAME: string = 'GetFeedbackForApplicationPageResult'; + public emails: string[]; + public items: GetFeedbackForApplicationPageResultItem[]; + public totalCount: number; +} +export class GetFeedbackForApplicationPageResultItem +{ + public static TYPE_NAME: string = 'GetFeedbackForApplicationPageResultItem'; + public emailAddress: string; + public incidentId: number; + public incidentName: string; + public message: string; + public writtenAtUtc: Date; +} +export class GetFeedbackForDashboardPage +{ + public static TYPE_NAME: string = 'GetFeedbackForDashboardPage'; +} +export class GetFeedbackForDashboardPageResult +{ + public static TYPE_NAME: string = 'GetFeedbackForDashboardPageResult'; + public emails: string[]; + public items: GetFeedbackForDashboardPageResultItem[]; + public totalCount: number; +} +export class GetFeedbackForDashboardPageResultItem +{ + public static TYPE_NAME: string = 'GetFeedbackForDashboardPageResultItem'; + public applicationId: number; + public applicationName: string; + public emailAddress: string; + public message: string; + public writtenAtUtc: Date; +} +export class GetIncidentFeedback +{ + public static TYPE_NAME: string = 'GetIncidentFeedback'; + public incidentId: number; +} +export class GetIncidentFeedbackResult +{ + public static TYPE_NAME: string = 'GetIncidentFeedbackResult'; + public emails: string[]; + public items: GetIncidentFeedbackResultItem[]; +} +export class GetIncidentFeedbackResultItem +{ + public static TYPE_NAME: string = 'GetIncidentFeedbackResultItem'; + public emailAddress: string; + public message: string; + public writtenAtUtc: Date; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Web/Overview.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Web/Overview.ts new file mode 100644 index 00000000..e99b05ec --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/Web/Overview.ts @@ -0,0 +1,34 @@ +export class GetOverview { + public static TYPE_NAME: string = 'GetOverview'; + public NumberOfDays: number; + public IncludeChartData: boolean = true; + public IncludePartitions: boolean; +} +export class GetOverviewApplicationResult { + public static TYPE_NAME: string = 'GetOverviewApplicationResult'; + public Label: string; + public Values: number[]; +} +export class GetOverviewResult { + public static TYPE_NAME: string = 'GetOverviewResult'; + public Days: number; + public IncidentsPerApplication: GetOverviewApplicationResult[]; + public StatSummary: OverviewStatSummary; + public TimeAxisLabels: string[]; + public MissedReports?: number; +} +export class OverviewStatSummary { + public static TYPE_NAME: string = 'OverviewStatSummary'; + public Followers: number; + public Incidents: number; + public NewestIncidentReceivedAtUtc?: Date; + public Reports: number; + public NewestReportReceivedAtUtc?: Date; + public UserFeedback: number; + public Partitions: PartitionOverview[]; +} +export class PartitionOverview { + public Name: string; + public DisplayName: string; + public Value: number; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/server-client.service.spec.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/server-client.service.spec.ts new file mode 100644 index 00000000..d0a794a9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/server-client.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { ServerClientService } from './server-client.service'; + +describe('ServerClientService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: ServerClientService = TestBed.get(ServerClientService); + expect(service).toBeTruthy(); + }); +}); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/server-client.service.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/server-client.service.ts new file mode 100644 index 00000000..f950dfa0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/server-api/server-client.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpHeaders } from "@angular/common/http"; +import { environment } from "../environments/environment"; + +@Injectable({ + providedIn: 'root' +}) +export class ServerClientService { + + constructor(private httpClient: HttpClient) { + if (!environment.apiUrl) { + throw new Error("environment.apiUrl must be specified"); + } + if (environment.apiUrl.substr(environment.apiUrl.length - 1, 1) !== "/") + environment.apiUrl += "/"; + } + async command(cmd: any): Promise { + + const httpOptions = { + headers: new HttpHeaders({ + 'Content-Type': 'application/json', + "X-Cqs-Name": cmd.constructor.TYPE_NAME + }) + }; + await this.httpClient.post(`${environment.apiUrl}cqs/`, cmd, httpOptions); + } + + async query(query: any): Promise { + const httpOptions = { + headers: new HttpHeaders({ + "Accept": "application/json", + 'Content-Type': 'application/json', + "X-Cqs-Name": query.constructor.TYPE_NAME + }) + }; + + var result = await this.httpClient.post(`${environment.apiUrl}cqs/`, query, httpOptions).toPromise(); + return result; + } + + async auth(): Promise { + const httpOptions = { + headers: new HttpHeaders({ + "Accept": "application/json", + }) + }; + + await this.httpClient.post(`${environment.apiUrl}authenticate/`, null, httpOptions); + var result = await this.httpClient.post(`${environment.apiUrl}authenticate/`, null, httpOptions).toPromise(); + return result.body; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/_mixins.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/_mixins.scss new file mode 100644 index 00000000..706aa2c7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/_mixins.scss @@ -0,0 +1,7 @@ +@mixin box-shadow-1 { + box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px; +} + +@mixin text-shadow-1 { + text-shadow: 2px 4px 3px rgba(0,0,0, 0.3); +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/backgrounds.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/backgrounds.scss new file mode 100644 index 00000000..cab4c7e6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/backgrounds.scss @@ -0,0 +1,25 @@ +@import "../_partials/coderr-variables.scss"; + +.bg { + &.white { + background-color: white; + } + &.dark{ + background-color: $dark; + } +} + +.alert { + padding: 10px; + + &.warning { + background-color: $red; + border: 1px solid darken($red, 10); + color: $light; + } + + &.success { + background-color: $blue-30; + border: 1px solid darken($blue-30, 10); + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/buttons.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/buttons.scss new file mode 100644 index 00000000..105caf48 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/buttons.scss @@ -0,0 +1,127 @@ +@import "coderr-variables.scss"; +$btn-color: white !default; + +.panel { + .btn { + background-color: $light; + color: $dark; + display: inline-flex; + } + + &.fill .btn, .fill .btn { + background-color: $blue; + border-color: $blue; + color: $light; + display: inline-flex; + } + + button[type="submit"], a.submit { + @extend .btn; + background-color: $light; + border-color: $light; + border: darken($light, 10%); + color: $dark; + width: inherit; + } + + button[type="reset"], a.reset { + @extend .btn; + color: $light; + background-color: $bg-accent; + border-color: $bg-accent; + border: darken($bg-accent, 10%); + } +} + + +a.btn, +button.btn, +input.button { + padding: 10px 12px; + margin-top: 15px; + margin-right: 5px; + border-radius: 3px; + text-decoration: none; + box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1); + text-shadow: 1px 1px rgba(0, 0, 0, 0.05); + text-align: center; + cursor: pointer; + display: inline-block; + + &.small { + margin-top: 5px; + margin-right: 2px; + padding: 4px 6px; + font-size: 0.9em; + } + + &.default { + background: $btn-color; + color: $dark; + } + + &.block { + display: block; + width: 100%; + } + + &.light { + background-color: $light; + color: $dark; + } + + &.dark { + background-color: $dark; + color: $light; + } + + &.red { + background-color: $red !important; + color: white; + border: 1px solid darken($red, 1%); + transition: 0.5s; + } + + &.red:hover { + background-color: darken($red, 15) !important; + } + + &.blue, &.default { + background-color: $blue !important; + color: white; + border: 1px solid darken($blue, 1%); + } + + &.red50 { + background-color: rgba($red, 0.75) !important; + color: white; + border: 1px solid rgba($red, 0.95); + } + + &.red-2 { + background-color: lighten($red, 5) !important; + color: white; + border: 1px solid rgba($red, 0.5); + } + + &.white { + background-color: white !important; + color: $gray-800; + border: 1px solid darken(white, 1%); + } + + &.gray { + background-color: $gray-300 !important; + color: $gray-900; + border: 1px solid darken(white, 1%); + + .btn { + background: $blue; + } + } + + &.dark { + background-color: $gray-800 !important; + color: $light; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/checkbox.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/checkbox.scss new file mode 100644 index 00000000..63c52138 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/checkbox.scss @@ -0,0 +1,48 @@ +@import "coderr-variables.scss"; + +.card { + input[type="checkbox"] { + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + outline: 0; + background: $gray-300; + height: 16px; + width: 16px; + border: 1px solid white; + } + + input[type="checkbox"]:checked { + background: $blue; + color: $blue !important; + } + + input[type="checkbox"]:hover { + filter: brightness(90%); + } + + input[type="checkbox"]:disabled { + background: $gray-700; + opacity: 0.6; + pointer-events: none; + } + + input[type="checkbox"]:after { + content: ''; + position: relative; + left: 40%; + top: 20%; + width: 15%; + height: 40%; + display: none; + } + + input[type="checkbox"]:checked:after { + display: block; + } + + input[type="checkbox"]:disabled:after { + border-color: $gray-100; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/coderr-variables.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/coderr-variables.scss new file mode 100644 index 00000000..d985cf2b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/coderr-variables.scss @@ -0,0 +1,51 @@ +$gray-100: #f4f4f4; +$gray-200: #ededed; +$gray-300: #eee; +$gray-400: #dddddd; +$gray-500: #706f6f; +$gray-600: #3c3c3b; +$gray-700: #393938; +$gray-800: #2e2d2c; +$gray-900: #141414; + + +$blue: #59c1d5; +$blue-60: #abdbe7; +$blue-30: #d8eef4; + +$red: #f18c65; +$red-80: #f3a383; +$red-60: #fac981; +$red-30: #feede5; + +$base-font-size: 16px; + +// Theme light + +$panel-bg: #f9f9f9; +$panel-text: $gray-900; + + +$text-primary: #fff; +$text-accent-1: $blue; +$text-accent-2: $red; + +$body-background: $blue; + +$bg-primary: $blue; +$bg-secondary: #fff; +$bg-accent: $red; + +$input-bg: white; +$input-border-color: $blue-60; + +$nav-bg: $gray-900; +$nav-text: $gray-400; +$nav-sub-bg: $gray-800; +$nav-sub-tab: darken($blue, 30%); + + +$light: $gray-200; +$dark: $gray-700; + +$font-family-sans-serif: "SofiaProRegular", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji" !default; diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/forms.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/forms.scss new file mode 100644 index 00000000..de45848c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/forms.scss @@ -0,0 +1,92 @@ +@import "coderr-variables.scss"; + +.ng-valid[required], .ng-valid.required { + border-left: 5px solid #42A948; /* green */ +} + +.ng-invalid:not(form), input:invalid { + border-left: 5px solid #a94442; /* red */ +} + +input:focus:invalid { + box-shadow: none; +} + +input, select, textarea { + padding: 4px; + border: 1px solid #404040; + border-radius: 3px; +} + +.form { + .form-group { + margin-top: 15px; + } + + label { + display: block; + font-weight: bold; + margin-top: 10px; + margin-bottom: 2px; + + &.inline { + display: inline; + } + } + + input { + &.inline { + display: inline; + } + } + + input[type="text"], + input[type="number"], + input[type="email"], + input[type="password"], + textarea, + select { + background-color: $input-bg; + border: 1px solid $input-border-color; + border-radius: 3px; + padding: 8px; + width: 100%; + margin-top: 5px; + } + + textarea { + min-height: 100px; + width: 100%; + } + + button { + display: inline-flex; + } + + button[type="submit"], a.submit { + @extend .btn; + background-color: $bg-primary; + border: darken($bg-primary, 20%); + color: $light; + width: inherit; + } + + button[type="reset"], a.reset { + @extend .btn; + color: $light; + background-color: darken($bg-primary, 10%); + border: darken($bg-accent, 20%); + } + + .ng-valid[required], .ng-valid.required { + border-left: 5px solid #42A948; /* green */ + } + + .ng-invalid:not(form), input:invalid { + border-left: 5px solid #a94442; /* red */ + } + + input:focus:invalid { + box-shadow: none; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/image.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/image.scss new file mode 100644 index 00000000..bd88c7db --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/image.scss @@ -0,0 +1,9 @@ +.svg { + &.blue { + filter: invert(77%) sepia(17%) saturate(1201%) hue-rotate(144deg) brightness(89%) contrast(87%); + } + + &.red { + filter: invert(57%) sepia(89%) saturate(365%) hue-rotate(326deg) brightness(98%) contrast(92%); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/layout.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/layout.scss new file mode 100644 index 00000000..d0586622 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/layout.scss @@ -0,0 +1,109 @@ + +.row { + display: flex; + width: 100%; + flex-flow: row wrap; + max-width: 100%; +} + +.container { + display: block; + max-width: 1400px; + margin: 0 auto; + padding: 30px; + + h1 { + font-size: $base-font-size*3; + } + + padding: -10px; +} + +.col { + justify-content: center; + align-items: start; + flex-basis: 0; + /*flex-direction: column; + flex-basis: auto;*/ + flex: 1 1 200px; + margin: 10px; + + /* pre > code wont wrap otherwise */ + min-width: 0; +} + +.col-2 { + flex: 2; +} + +.col-3 { + flex: 3; +} + +.hidden { + display: none; + opacity: 0; +} + +.w-100 { + width: 100%; +} + +@media only screen and (max-width: 600px) { + .col { + display: block; + width: 100%; + } +} + +.y-center { + align-items: center; +} + +.x-center { + justify-content: center; +} + +.mx-auto { + margin: 0 auto; +} + +@for $i from 0 through 5 { + .mt-#{$i} { + margin-top: $i*5px; + } + + .mb-#{$i} { + margin-bottom: $i*5px; + } + + .ml-#{$i} { + margin-left: $i*5px !important; + } + + .m-#{$i} { + margin: $i*5px; + } + + .pt-#{$i} { + padding-top: $i*5px; + } + + .pl-#{$i} { + padding-left: $i*5px; + } + + + .pr-#{$i} { + padding-right: $i*5px; + } + + + .pb-#{$i} { + padding-bottom: $i*5px; + } + + .p-#{$i} { + padding: $i*5px; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/radio.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/radio.scss new file mode 100644 index 00000000..4b512f2b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/radio.scss @@ -0,0 +1,48 @@ +@import "coderr-variables.scss"; + +.card { + input[type="radio"] { + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + outline: 0; + background: $gray-300; + height: 16px; + width: 16px; + border: 1px solid white; + } + + input[type="radio"]:checked { + background: $blue; + color: $blue !important; + } + + input[type="radio"]:hover { + filter: brightness(90%); + } + + input[type="radio"]:disabled { + background: $gray-700; + opacity: 0.6; + pointer-events: none; + } + + input[type="radio"]:after { + content: ''; + position: relative; + left: 40%; + top: 20%; + width: 15%; + height: 40%; + display: none; + } + + input[type="radio"]:checked:after { + display: block; + } + + input[type="radio"]:disabled:after { + border-color: $gray-100; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/reset.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/reset.scss new file mode 100644 index 00000000..464bbd5b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/reset.scss @@ -0,0 +1,25 @@ +html { + box-sizing: border-box; + font-size: 15px; + font-family: "Sofia Pro", sans-serif; + height: 100%; +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body, h1, h2, h3, h4, h5, h6, p, ol, ul { + margin: 0; + padding: 0; + font-weight: normal; +} + +ol, ul { + list-style: none; +} + +img { + max-width: 100%; + height: auto; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/tables.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/tables.scss new file mode 100644 index 00000000..d8d83eab --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/tables.scss @@ -0,0 +1,35 @@ +@import "coderr-variables.scss"; + +.table { + table-layout: fixed; + display: table; + + thead tr { + background-color: $bg-secondary; + + &.dark { + color: $light; + } + + + th { + text-align: left; + padding: 5px; + } + } + + &.striped tbody { + tr:nth-child(even) { + background-color: lighten($bg-secondary, 25%); + } + } + + &.dark { + color: $light; + } + + + tbody tr td { + padding: 5px; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/topnav.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/topnav.scss new file mode 100644 index 00000000..cb649867 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/topnav.scss @@ -0,0 +1,92 @@ +@import "coderr-variables.scss"; + +header { + background: $nav-bg; + margin: 0; + padding: 5px; + /*.main { + box-shadow: 0 15px 5px lighten($nav-bg, 20%); + } +*/ + img { + vertical-align: middle; + height: 25px; + margin-top: -5px; + } + + ul { + list-style-type: none; + display: inline-block; + + li { + color: $nav-text; + display: inline-block; + margin-left: 20px; + + a { + text-decoration: none; + color: $nav-text; + } + + a:hover { + text-shadow: 0px 0px 2px rgba(255, 255, 255, 0.1), 0px 2px 3px rgba(255, 255, 255, 0.5), 0px 6px 6px rgba(255, 255, 255, 0.2); + color: $text-accent-1; + } + } + } + + .box-shadow { + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.05); + } + + .left-menu { + flex-grow: 1; + align-self: center; + padding: 5px; + color: #999; + + a { + padding-left: 5px; + padding-right: 5px; + color: white; + text-decoration: none; + font-size: 14px; + } + } + + .right-menu { + align-self: center; + } + + .submenu { + background: $nav-sub-bg; + padding-top: 10px; + padding-left: 60px; + + .groups { + margin-top: 10px; + margin-bottom: 10px; + + a { + background-color: $nav-sub-tab; + padding: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; + } + } + + a { + color: $nav-text; + text-decoration: none; + } + + .application-list { + display: flex; + flex-direction: column; + } + + .application-list div { + padding: 5px; + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/typography.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/typography.scss new file mode 100644 index 00000000..25b3ee5b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/_partials/typography.scss @@ -0,0 +1,65 @@ +@import "_mixins.scss"; +@import "coderr-variables.scss"; + +span.muted { + color: $gray-500; +} +span.small { + font-size: 0.9em; +} + +a +{ + color: $red; + text-decoration: none; +} + +p { + margin-top: 5px; + margin-bottom: 15px; +} + +.text-shadow-1 { + @include text-shadow-1 +} + +h2, h3, h4 { + font-weight: 600; + margin-top: 10px; + margin-bottom: 10px; +} + +.text-center { + text-align: center; +} + +.text-right{ + text-align: right; +} +.text-left { + text-align: left; +} + +.text-blue { + color: $blue; +} + +.text-dark { + color: $dark; +} + +.text-light { + color: $light; +} + +.text-red { + color: $red; +} + +.text-white { + color: #fff; +} + +.text-muted { + color: #bbb; +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/site.scss b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/site.scss new file mode 100644 index 00000000..b61b235c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/styles/site.scss @@ -0,0 +1,256 @@ +@import "_partials/reset.scss"; +@import "_partials/coderr-variables.scss"; +@import "_partials/_mixins.scss"; +//@import '~toastr/toastr.scss'; +@import "_partials/radio.scss"; +@import "_partials/backgrounds.scss"; +@import "_partials/checkbox.scss"; +@import "_partials/forms.scss"; +@import "_partials/layout.scss"; +@import "_partials/buttons.scss"; +@import "_partials/topnav.scss"; +@import "_partials/image.scss"; +@import "_partials/typography.scss"; +@import "_partials/tables.scss"; + +body { + background-color: $blue; +} + +.main-view { + /**padding: 10px; Let all views control this **/ +} + +.starter { + margin: 10px; + padding: 0; + + > .panel, > .panels { + margin: -10px !important; + padding: 0; + } +} + +.p-10 { + padding: 10px; +} + +.fb-300px { + flex-basis: 400px; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-grow-0 { + flex-grow: 0; +} + +.panels { + display: flex; + flex-wrap: wrap; + flex-direction: row; + + .panel { + margin: 10px; + padding: 0; + flex: 1 0 auto; + flex-basis: 30%; + max-width: 50%; + } +} + +.panel { + color: $panel-text; + flex: 1; + margin: 10px; + padding: 0; + border-radius: 3px; + flex-direction: column; + flex-basis: 50%; + min-width: 400px; + display: flex; + + > * { + display: block; + box-sizing: border-box; + } + + &.full { + flex: 1 1 100% + } + + > h1, > h2, > h3 { + color: white; + @include text-shadow-1; + } + + &.fill, .fill { + background-color: $panel-bg; + @include box-shadow-1; + border-radius: 3px; + flex: 1; + padding: 15px; + display: block; + + h3 { + color: $dark; + } + } + + > .table { + background-color: $panel-bg; + padding: 15px; + } + + .panel-header { + font-size: $base-font-size * 1.5; + padding: 10px; + width: 100%; + } + + .panel-body { + padding: 10px; + flex-grow: 1; + width: 100%; + } + + .panel-footer { + padding: 10px; + width: 100%; + } +} + + +.col .panel { + margin-left: 0; + margin-right: 0; +} + +.f-grow { + flex-grow: 1; +} + +.facts { + + th { + font-weight: 500; + text-align: right; + min-width: 150px; + } +} + +.dropdown { + position: relative; + display: inline-block; + + .content { + display: none; + position: absolute; + z-index: 2; + background-color: #fff; + padding: 10px; + border: 1px solid #333; + box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px; + + &.show { + display: block; + } + } +} + +.pointer { + cursor: pointer; +} + +.menu { + visibility: hidden; + position: fixed; + background-color: teal; + word-wrap: unset; + margin: 0; + transition: 0.5s; + opacity: 0; + padding: 10px; + border: red 1px dashed; + + &.shown { + opacity: 1; + visibility: visible; + } +} + +h2.active { + border-bottom: orange 3px solid; + color: white; +} + + +/** Guide CSS */ + +.dimScreen { + display: none; + position: fixed; + padding: 0; + margin: 0; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 10; +} + +#note { + display: none; + /*background-color: #FDFF47;*/ + background: $blue-30; + color: #000; + position: absolute; + z-index: 11; + padding: 10px; + min-width: 250px; + box-shadow: rgba(255, 255, 255, 0.4) 0px 8px 24px; + border-radius: 5px; + border: 10px solid; + border-image-slice: 1; + border-width: 5px; + border-image-source: linear-gradient(to left, $blue, $blue-60); + + > div { + margin-bottom: 20px; + } + + button { + cursor: pointer; + font-size: 0.8em; + float: right; + background: $blue; + border-radius: 2px; + border: 5px solid; + border-image-slice: 1; + border-width: 2px; + border-image-source: linear-gradient(to left, $blue, $blue-60); + } + + h3 { + margin-top: 5px; + } +} + +.guide-highlight { + position: relative; + background-color: #fff !important; + z-index: 11; + box-shadow: rgba(255, 255, 255, 0.4) 0px 8px 24px; + border-radius: 2px; + padding: 4px; + color: $blue !important; + + i { + background-color: #fff !important; + color: $blue !important; + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/test.ts b/src/Server/Coderr.Server.WebSite/ClientApp/src/test.ts new file mode 100644 index 00000000..a6f15af3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/test.ts @@ -0,0 +1,20 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: any; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.app.json b/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.app.json new file mode 100644 index 00000000..b1fca33f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.app.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "outDir": "../out-tsc/app", + "types": [] + }, + "files": [ + "main.ts", + "polyfills.ts" + ], + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.server.json b/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.server.json new file mode 100644 index 00000000..3f183ef3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.server.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "module": "commonjs" + }, + "angularCompilerOptions": { + "entryModule": "app/app.server.module#AppServerModule" + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.spec.json b/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.spec.json new file mode 100644 index 00000000..de773363 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/tsconfig.spec.json @@ -0,0 +1,18 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "outDir": "../out-tsc/spec", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts", + "polyfills.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/src/tslint.json b/src/Server/Coderr.Server.WebSite/ClientApp/src/tslint.json new file mode 100644 index 00000000..52e2c1a5 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/src/tslint.json @@ -0,0 +1,17 @@ +{ + "extends": "../tslint.json", + "rules": { + "directive-selector": [ + true, + "attribute", + "app", + "camelCase" + ], + "component-selector": [ + true, + "element", + "app", + "kebab-case" + ] + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/tsconfig.json b/src/Server/Coderr.Server.WebSite/ClientApp/tsconfig.json new file mode 100644 index 00000000..53892b0e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "module": "esnext", + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "experimentalDecorators": true, + "target": "es2015", + "typeRoots": [ + "node_modules/@types" + ], + "lib": [ + "es2017", + "dom" + ] + } +} diff --git a/src/Server/Coderr.Server.WebSite/ClientApp/tslint.json b/src/Server/Coderr.Server.WebSite/ClientApp/tslint.json new file mode 100644 index 00000000..f5f06e9e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ClientApp/tslint.json @@ -0,0 +1,130 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "arrow-return-shorthand": true, + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "deprecation": { + "severity": "warn" + }, + "eofline": true, + "forin": true, + "import-blacklist": [ + true, + "rxjs/Rx" + ], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + { + "order": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-super": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [ + true, + "ignore-params" + ], + "no-misused-new": true, + "no-non-null-assertion": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unnecessary-initializer": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + true, + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + "no-output-on-prefix": true, + "no-inputs-metadata-property": true, + "no-outputs-metadata-property": true, + "no-host-metadata-property": true, + "no-input-rename": true, + "no-output-rename": true, + "use-lifecycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true + } +} diff --git a/src/Server/Coderr.Server.WebSite/Coderr.Server.WebSite.csproj b/src/Server/Coderr.Server.WebSite/Coderr.Server.WebSite.csproj new file mode 100644 index 00000000..41d117d6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Coderr.Server.WebSite.csproj @@ -0,0 +1,84 @@ + + + + netcoreapp3.1 + true + Latest + false + ClientApp\ + 3.0-rc01 + $(DefaultItemExcludes);$(SpaRoot)node_modules\** + + + false + 0b5b30d5-8b80-4926-a654-b5a56a650815 + Debug;Release + + + + TRACE;DEBUG + + + + + PreserveNewest + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + %(DistFiles.Identity) + PreserveNewest + true + + + + + diff --git a/src/Server/Coderr.Server.WebSite/Controllers/AccountController.cs b/src/Server/Coderr.Server.WebSite/Controllers/AccountController.cs new file mode 100644 index 00000000..6e3c9cb9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/AccountController.cs @@ -0,0 +1,342 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Security.Authentication; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Accounts.Commands; +using Coderr.Server.Api.Core.Accounts.Requests; +using Coderr.Server.Api.Core.Applications.Queries; +using Coderr.Server.Api.Core.Invitations.Queries; +using Coderr.Server.App.Core.Accounts; +using Coderr.Server.Infrastructure.Configuration; +using Coderr.Server.WebSite.Infrastructure; +using Coderr.Server.WebSite.Models.Accounts; +using DotNetCqs; +using Griffin.Data; +using log4net; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; + +namespace Coderr.Server.WebSite.Controllers +{ + /// + /// TODO: Break out logic + /// + [AllowAnonymous] + public class AccountController : Controller + { + private readonly IAccountService _accountService; + private readonly IAdoNetUnitOfWork _uow; + private readonly ILog _logger = LogManager.GetLogger(typeof(AccountController)); + private readonly IMessageBus _messageBus; + private readonly IQueryBus _queryBus; + private readonly ConfigurationStore _configStore; + + public AccountController(IAccountService accountService, IMessageBus messageBus, IAdoNetUnitOfWork uow, IQueryBus queryBus, ConfigurationStore configStore) + { + _accountService = accountService; + _messageBus = messageBus; + _uow = uow; + _queryBus = queryBus; + _configStore = configStore; + } + + /// + /// Accept invitation + /// + /// + /// + [HttpGet("api/account/accept/{id}")] + public async Task Accept(string id) + { + try + { + var query = new GetInvitationByKey(id); + var invitation = await _queryBus.QueryAsync(query); + if (invitation == null) + return View("InvitationNotFound"); + + var model = new AcceptViewModel + { + InvitationKey = id, + Email = invitation.EmailAddress + }; + + return View(model); + } + catch (Exception exception) + { + ModelState.AddModelError("", exception.Message); + _logger.Error("Failed ot launch", exception); + return View(new AcceptViewModel()); + } + } + + [HttpPost("api/account/accept")] + public async Task Accept(AcceptViewModel model) + { + if (!ModelState.IsValid) + { + return new LoginResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + + var cmd = new AcceptInvitation(model.UserName, model.Password, model.InvitationKey) + { + AcceptedEmail = model.Email, + FirstName = model.FirstName, + LastName = model.LastName + }; + + var identity = await _accountService.AcceptInvitation(User, cmd); + + //TODO: Remove hack. + // HERE since the message queue starts to process the events + // before we are done with them. We need some way to stack up the publishing + // until the current handler is done. + // + // can't use a message handler since we need a result from the invitation accept. + // so that we can construct a new identity + _uow.SaveChanges(); + + if (identity == null) + { + ModelState.AddModelError("", + "Failed to find an invitation with the specified key. You might have already accepted the invitation? If not, ask for a new one."); + _logger.Error("Failed to find invitation " + model.InvitationKey); + return new LoginResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + + + var token = JwtHelper.GenerateToken(identity); + return new LoginResult { Success = true, JwtToken = token }; + } + + [HttpPost("api/account/activate/{id}")] + public async Task Activate(string id, string returnUrl = null) + { + try + { + _logger.Debug("Activating " + id); + var identity = await _accountService.ActivateAccount(User, id); + _logger.Debug("Signin " + id); + var token = JwtHelper.GenerateToken(identity); + return new LoginResult { Success = true, JwtToken = token }; + + } + catch (ArgumentOutOfRangeException ex) + { + _logger.Warn("Failed to activate using " + id, ex); + ModelState.AddModelError("", "Activation key was not found."); + } + catch (Exception err) + { + _logger.Warn("Failed to activate using " + id, err); + ModelState.AddModelError("", err.Message); + } + + return new LoginResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + + [HttpGet("account/activation/requested")] + public ActionResult ActivationRequested() + { + return View(); + } + + [HttpPost("api/account/login")] + public async Task Login([FromBody] LoginViewModel model) + { + var config = _configStore.Load(); + model.AllowRegistrations = config.AllowRegistrations != false; + + if (!ModelState.IsValid) + { + return new LoginResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + + try + { + var identity = await _accountService.Login(model.UserName, model.Password); + if (identity == null) + { + ModelState.AddModelError("", "Incorrect username or password."); + model.Password = ""; + return new LoginResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + var token = JwtHelper.GenerateToken(identity); + return new LoginResult { Success = true, JwtToken = token }; + } + catch (AuthenticationException err) + { + _logger.Error("Failed to authenticate", err); + ModelState.AddModelError("", err.Message); + return new LoginResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + catch (Exception exception) + { + _logger.Error("Failed to authenticate", exception); + ModelState.AddModelError("", "Failed to authenticate"); + return new LoginResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + } + + + [HttpGet("api/account/logout")] + public ActionResult Logout() + { + HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + if (ServerConfig.Instance.IsLive) + return Redirect("https://lobby.coderr.io/"); + + return Redirect("~/"); + } + + [HttpGet("api/account/register")] + public ActionResult Register() + { + var model = new RegisterViewModel { ReturnUrl = Request.Query["ReturnUrl"].FirstOrDefault() }; + return View(model); + } + + + [HttpPost("api/account/register")] + public async Task Register([FromBody] RegisterViewModel model) + { + var config = _configStore.Load(); + if (config.AllowRegistrations == false) + { + ModelState.AddModelError("", "New registrations are not allowed."); + } + + if (!ModelState.IsValid) + { + return new RegisterResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + + + try + { + var reply = await _accountService.ValidateLogin(model.Email, model.UserName); + + if (reply.UserNameIsTaken) + ModelState.AddModelError("UserName", "Username is already in use."); + + if (reply.EmailIsTaken) + ModelState.AddModelError("Email", "Email address is already in use."); + + if (!ModelState.IsValid) + return new RegisterResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + + // This is really a workaround, but the UnitOfWork that wraps + // this action method deadlocks our transaction in the message bus, + // thus we need to tell that the outer UoW is done. + _uow.SaveChanges(); + + await + _messageBus.SendAsync(User, new RegisterAccount(model.UserName, model.Password, model.Email) + { + ReturnUrl = model.ReturnUrl + }); + } + catch (Exception exception) + { + ModelState.AddModelError("UserName", exception.Message); + return new RegisterResult { Success = false, ErrorMessage = ModelState.ToSummary() }; + } + + + return new RegisterResult { Success = true, VerificationIsRequested = true }; + } + + [HttpGet("password/request/reset")] + public ActionResult RequestPasswordReset() + { + return View(); + } + + [HttpPost("api/password/reset")] + public async Task RequestPasswordReset(RequestPasswordResetViewModel model) + { + if (!ModelState.IsValid) + return View(); + + var cmd = new RequestPasswordReset(model.EmailAddress); + await _messageBus.SendAsync(User, cmd); + + return View("PasswordRequestReceived"); + } + + [HttpGet("api/password/reset/{activationKey}")] + public ActionResult ResetPassword(string activationKey) + { + return View(new ResetPasswordViewModel { ActivationKey = activationKey }); + } + + [HttpPost("api/password/reset/{activationKey}")] + public async Task ResetPassword(ResetPasswordViewModel model) + { + if (model.Password != model.Password2) + { + ModelState.AddModelError("Password2", "Passwords must match."); + } + if (!ModelState.IsValid) + return View(model); + + try + { + var found = await _accountService.ResetPassword(model.ActivationKey, model.Password); + if (!found) + { + ModelState.AddModelError("", "Activation key was not found."); + return View(model); + } + } + catch (Exception exception) + { + ModelState.AddModelError("", exception.Message); + _logger.Error("Failed to reset password using key " + model.ActivationKey, exception); + return View(model); + } + + var drDictionary = + new RouteValueDictionary { { "usernote", "Password have been changed, you may now login." } }; + return RedirectToAction("Login", drDictionary); + } + + [Authorize, HttpPost("account/update/token")] + public async Task UpdateSession() + { + var getApps = new GetApplicationList { AccountId = User.GetAccountId() }; + var apps = await _queryBus.QueryAsync(User, getApps); + + var currentClaims = User.Claims + .Where(x => x.Type != CoderrClaims.Application && x.Type != CoderrClaims.ApplicationAdmin) + .ToList(); + + foreach (var app in apps) + { + var claim = new Claim(CoderrClaims.Application, app.Id.ToString()); + currentClaims.Add(claim); + + if (app.IsAdmin) + { + claim = new Claim(CoderrClaims.ApplicationAdmin, app.Id.ToString()); + currentClaims.Add(claim); + } + } + + var identity = new ClaimsIdentity(currentClaims, "Cookies"); + var token = JwtHelper.GenerateToken(identity); + + return new LoginResult { Success = true, JwtToken = token }; + } + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Controllers/AuthenticationController.cs b/src/Server/Coderr.Server.WebSite/Controllers/AuthenticationController.cs new file mode 100644 index 00000000..9ef82b92 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/AuthenticationController.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Controllers +{ + public class AuthenticationController : Controller + { + public IActionResult Login() + { + return View(); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Controllers/CqsController.cs b/src/Server/Coderr.Server.WebSite/Controllers/CqsController.cs new file mode 100644 index 00000000..c8fa5360 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/CqsController.cs @@ -0,0 +1,321 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Runtime.ExceptionServices; +using System.Security.Authentication; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Api.Core.Applications.Commands; +using Coderr.Server.Api.Core.Applications.Queries; +using Coderr.Server.WebSite.Infrastructure.Cqs; +using Coderr.Server.WebSite.Infrastructure.Cqs.Adapters; +using Coderr.Server.WebSite.Models; +using DotNetCqs; +using Griffin; +using Griffin.Data; +using Griffin.Net.Protocols.Http; +using log4net; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Coderr.Server.WebSite.Controllers +{ + [Authorize] + public class CqsController : Controller + { + internal static readonly CqsObjectMapper _cqsObjectMapper = new CqsObjectMapper(); + private static readonly MethodInfo _queryMethod; + private static readonly MethodInfo _sendMethod; + private readonly ILog _logger = LogManager.GetLogger(typeof(CqsController)); + private readonly IMessageBus _messageBus; + private readonly IQueryBus _queryBus; + private readonly IPrincipalAccessor _principalSetter; + + static CqsController() + { + if (_cqsObjectMapper.IsEmpty) + { + _cqsObjectMapper.ScanAssembly(typeof(CreateApplication).Assembly); + } + + + _queryMethod = typeof(IQueryBus) + .GetMethods(BindingFlags.Public | BindingFlags.Instance) + .First(x => x.Name == "QueryAsync" && x.GetParameters().Length == 2); + + _sendMethod = typeof(IMessageBus).GetMethod("SendAsync", new[] { typeof(ClaimsPrincipal), typeof(object) }); + } + + public CqsController(ExecuteDirectlyMessageBus messageBus, IQueryBus queryBus, IPrincipalAccessor principalSetter) + { + _messageBus = messageBus; + _queryBus = queryBus; + _principalSetter = principalSetter; + } + + [HttpPost("api/authenticate")] + [HttpPost("authenticate")] + public async Task Authenticate() + { + if (!User.Identity.IsAuthenticated) + return Unauthorized(); + + _logger.Info("Authenticating " + User.GetAccountId()); + try + { + + var q = new GetApplicationList { AccountId = User.GetAccountId() }; + var result = await _queryBus.QueryAsync(User, q); + + + var dto = new AuthenticatedUser(User.GetAccountId(), User.Identity.Name) + { + Applications = result, + IsSysAdmin = User.IsInRole(CoderrRoles.SysAdmin), + LicenseText = ""//TODO: LicenseWrapper.Instance?.LicenseInformation ?? " + }; + + _logger.Debug("Result: " + JsonConvert.SerializeObject(dto)); + return Json(dto); + } + catch (Exception ex) + { + _logger.Error("Failed to authenticate " + User.Identity.Name, ex); + throw; + } + + } + [HttpGet] + [HttpPost] + [Route("api/cqs")] + [Route("cqs")] + public async Task Cqs() + { + if (!HostConfig.Instance.IsConfigured) + { + return StatusCode((int)HttpStatusCode.ServiceUnavailable); + } + + var headerValue = Request.Headers["X-Cqs-Object-Type"]; + var dotNetType = headerValue.Count > 0 + ? headerValue[0] + : null; + headerValue = Request.Headers["X-Cqs-Name"]; + var cqsName = headerValue.Count > 0 + ? headerValue[0] + : null; + + if (!_cqsObjectMapper.HasType(cqsName)) + { + return StatusCode((int)HttpStatusCode.NotImplemented); + } + + var sw = Stopwatch.StartNew(); + string json; + using (var reader + = new StreamReader(Request.Body, Encoding.UTF8, true, 8192, true)) + { + json = await reader.ReadToEndAsync(); + } + + object cqsObject, cqsReplyObject = null; + if (!string.IsNullOrEmpty(dotNetType)) + { + cqsObject = _cqsObjectMapper.Deserialize(dotNetType, json); + if (cqsObject == null) + { + _logger.Error($"Could not deserialize[{dotNetType}]: {json}"); + return BadRequest(new ErrorMessage($"Unknown type: {dotNetType}")); + } + } + else if (!string.IsNullOrEmpty(cqsName)) + { + cqsObject = _cqsObjectMapper.Deserialize(cqsName, json); + if (cqsObject == null) + { + _logger.Error($"Could not deserialize [{cqsName}]: {json}"); + return BadRequest(new ErrorMessage($"Unknown type: {cqsName}")); + } + } + else + { + _logger.Error($"Did not find header for [{cqsName}]: {json}"); + return BadRequest(new ErrorMessage( + "Expected a class name in the header 'X-Cqs-Name' or a .NET type name in the header 'X-Cqs-Object-Type'.")); + } + + if (User.Identity.AuthenticationType != "ApiKey") + { + var prop = cqsObject.GetType().GetProperty("CreatedById"); + if (prop != null && prop.CanWrite) + prop.SetValue(cqsObject, User.GetAccountId()); + prop = cqsObject.GetType().GetProperty("AccountId"); + if (prop != null && prop.CanWrite) + prop.SetValue(cqsObject, User.GetAccountId()); + prop = cqsObject.GetType().GetProperty("UserId"); + if (prop != null && prop.CanWrite) + prop.SetValue(cqsObject, User.GetAccountId()); + } + + + RestrictOnApplicationId(cqsObject); + _principalSetter.Principal = User as ClaimsPrincipal; + + Exception ex = null; + try + { + _logger.Debug("Invoking " + cqsObject.GetType().Name + ", json: " + json.Replace("\r\n", " ")); + if (RegisterCqsServices.IsQuery(cqsObject)) + cqsReplyObject = await InvokeQuery(cqsObject); + else + await InvokeMessage(cqsObject); + if (cqsReplyObject != null) + RestrictOnApplicationId(cqsReplyObject); + + //await HandleSecurityPrincipalUpdates(); + } + catch (AggregateException e1) + { + _logger.Error("Failed to process '" + json + "'.", e1); + ex = e1.InnerException; + } + catch (Exception e1) + { + _logger.Error("Failed to process2 '" + json + "'.", e1); + ex = e1; + } + sw.Stop(); + if (sw.ElapsedMilliseconds > 200) + { + _logger.Info($"Took {sw.ElapsedMilliseconds}ms: {json}"); + } + + if (ex is EntityNotFoundException || ex is Griffin.Data.EntityNotFoundException) + { + _logger.Error("Entity not found " + json, ex); + return NotFound(); + } + if (ex is InvalidCredentialException) + { + _logger.Error("Auth error for " + json, ex); + var authEx = (InvalidCredentialException)ex; + return BadRequest(new ErrorMessage(FirstLine(ex.Message))); + } + if (ex != null) + { + _logger.Error("Failed to process result for " + json, ex); + return BadRequest(new ErrorMessage(FirstLine(ex.Message))); + } + + var result = new ContentResult { ContentType = "application/json" }; + + // for instance commands do not have a return value. + if (cqsReplyObject != null) + { + Response.Headers.Add("X-Cqs-Object-Type", cqsReplyObject.GetType().GetSimpleAssemblyQualifiedName()); + Response.Headers.Add("X-Cqs-Name", cqsReplyObject.GetType().Name); + if (cqsReplyObject is Exception) + result.StatusCode = 500; + + json = _cqsObjectMapper.Serialize(cqsReplyObject); + _logger.Debug("Reply to " + cqsObject.GetType().Name + ": " + json); + result.Content = json; + } + else + { + _logger.Debug("Reply to " + cqsObject.GetType().Name + ": [empty response]"); + result.StatusCode = (int)HttpStatusCode.NoContent; + } + + return result; + } + + private string FirstLine(string msg) + { + var pos = msg.IndexOfAny(new[] { '\r', '\n' }); + return pos == -1 ? msg : msg.Substring(0, pos); + } + + //private async Task HandleSecurityPrincipalUpdates() + //{ + // var gotUpdate = User.Identities.First().TryRemoveClaim(CoderrClaims.UpdateIdentity); + + // //to be sure that there are no other points in the flow that added the same claim + // while (User.Identities.First().TryRemoveClaim(CoderrClaims.UpdateIdentity)) + // { + // } + + // if (gotUpdate) + // { + // var usr = User; + // SignIn(usr, CookieAuthenticationDefaults.AuthenticationScheme); + // } + //} + + private async Task InvokeMessage(object dto) + { + var type = dto.GetType(); + try + { + var task = (Task)_sendMethod.Invoke(_messageBus, new[] { User, dto }); + await task; + } + catch (TargetInvocationException exception) + { + ExceptionDispatchInfo.Capture(exception.InnerException).Throw(); + throw; + } + } + + private async Task InvokeQuery(object dto) + { + var type = dto.GetType(); + var replyType = type.BaseType.GetGenericArguments()[0]; + var method = _queryMethod.MakeGenericMethod(replyType); + try + { + var result = method.Invoke(_queryBus, new[] { User, dto }); + var task = (Task)result; + await task; + return ((dynamic)task).Result; + } + catch (TargetInvocationException exception) + { + ExceptionDispatchInfo.Capture(exception.InnerException).Throw(); + throw; + } + } + + private void RestrictOnApplicationId(object cqsObject) + { + if (User.Identity.AuthenticationType != "ApiKey") + return; + if (User.IsInRole(CoderrRoles.SysAdmin)) + return; + + var prop = cqsObject.GetType().GetProperty("ApplicationId"); + if (prop == null || !prop.CanRead) + return; + + //appId can for instance be null in the GetApplication query + var value = prop.GetValue(cqsObject); + if (value == null) + return; + + var appId = (int)value; + if (appId != 0 && !User.IsApplicationMember(appId)) + { + _logger.Warn("Tried to access an application without privileges. accountId: " + User.Identity.Name + + ", appId: " + value); + throw new HttpException(403, "The given application key is not allowed for application " + value); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Controllers/FeedbackReceiverController.cs b/src/Server/Coderr.Server.WebSite/Controllers/FeedbackReceiverController.cs new file mode 100644 index 00000000..c4fb1930 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/FeedbackReceiverController.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Text; +using System.Threading.Tasks; +using Coderr.Client.Contracts; +using Coderr.Server.Domain.Core.Applications; +using Coderr.Server.Infrastructure.Messaging; +using Coderr.Server.ReportAnalyzer.Abstractions.Inbound.Commands; +using DotNetCqs; +using DotNetCqs.Queues; +using log4net; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Coderr.Server.WebSite.Controllers +{ + public class FeedbackReceiverController : Controller + { + private readonly IApplicationRepository _applicationRepository; + private readonly ILog _logger = LogManager.GetLogger(typeof(FeedbackReceiverController)); + private readonly IMessageQueue _queue; + + public FeedbackReceiverController(IMessageQueueProvider queueProvider, IApplicationRepository applicationRepository) + { + _applicationRepository = applicationRepository; + _queue = queueProvider.Open("ErrorReports"); + } + + [HttpPost, Route("receiver/report/{appKey}/feedback")] + public async Task SupplyFeedback(string appKey, string sig) + { + + var json = await UnpackContent(); + + try + { + var ser = new MessagingSerializer(); + var model = (FeedbackDTO)ser.Deserialize(typeof(FeedbackDTO), json); + + var app = await _applicationRepository.GetByKeyAsync(appKey); + using (var session = _queue.BeginSession()) + { + var dto = new ProcessFeedback + { + ApplicationId = app.Id, + Description = model.Description, + EmailAddress = model.EmailAddress, + ReceivedAtUtc = DateTime.UtcNow, + RemoteAddress = Request.HttpContext.Connection.RemoteIpAddress.ToString(), + ReportId = model.ReportId, + ReportVersion = "1" + }; + + await session.EnqueueAsync(ReportReceiverController.CreateReporterPrincipal(), new Message(dto)); + await session.SaveChanges(); + } + } + catch (Exception ex) + { + _logger.Warn( + "Failed to submit feedback: " + JsonConvert.SerializeObject(new { appKey, json }), + ex); + } + + return NoContent(); + } + + private async Task UnpackContent() + { + var buffer = new byte[HttpContext.Request.ContentLength.Value]; + await Request.Body.ReadAsync(buffer, 0, buffer.Length); + + // not compressed. + if (buffer[0] == '{') + { + return Encoding.UTF8.GetString(buffer); + } + + var ms1 = new MemoryStream(buffer, 0, buffer.Length); + var ms2 = new MemoryStream(buffer.Length); + using (var zipStream = new GZipStream(ms1, CompressionMode.Decompress, true)) + { + await zipStream.CopyToAsync(ms2); + } + + ms2.Position = 0; + var sr = new StreamReader(ms2, Encoding.UTF8); + return await sr.ReadToEndAsync(); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Controllers/GoController.cs b/src/Server/Coderr.Server.WebSite/Controllers/GoController.cs new file mode 100644 index 00000000..fb0c33e2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/GoController.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using Coderr.Server.Domain.Core.Incidents; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Controllers +{ + public class GoController : Controller + { + private readonly IIncidentRepository _incidentRepository; + + public GoController(IIncidentRepository incidentRepository) + { + _incidentRepository = incidentRepository; + } + + [HttpGet, Authorize] + public async Task Incident(int id) + { + var incident = await _incidentRepository.GetAsync(id); + return Redirect(incident.State == IncidentState.Active + ? $"/analyze/incident/{id}" + : $"/discover/incidents/{incident.ApplicationId}/incident/{id}"); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Controllers/HomeController.cs b/src/Server/Coderr.Server.WebSite/Controllers/HomeController.cs new file mode 100644 index 00000000..039a2a37 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/HomeController.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Boot; +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Controllers +{ + public class HomeController : Controller + { + private IConfiguration _configuration; + + public HomeController(IConfiguration configuration) + { + _configuration = configuration; + } + + public IActionResult Index() + { + if (!HostConfig.Instance.IsConfigured) + { + return Redirect("~/Installation"); + } + + return View(); + } + [HttpGet("wazzaa")] + public IActionResult Wazzaa() + { + if (!HostConfig.Instance.IsConfigured) + { + return NoContent(); + } + + return Ok(_configuration["LoginUrl"] ?? "/account/login"); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Controllers/OnboardingController.cs b/src/Server/Coderr.Server.WebSite/Controllers/OnboardingController.cs new file mode 100644 index 00000000..5af48e92 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/OnboardingController.cs @@ -0,0 +1,74 @@ +using System; +using System.Net.Http; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Infrastructure.Configuration; +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Controllers +{ + [Route("api/[controller]")] + public class OnboardingController : Controller + { + private IConfiguration _baseConfiguration; + + public OnboardingController(IConfiguration baseConfiguration) + { + _baseConfiguration = baseConfiguration; + } + + [HttpGet("[action]")] + public async Task Libraries() + { + var client = new HttpClient(); + var result = await client.GetStringAsync("https://coderr.io/configure/packages/"); + return Content(result, "application/json", Encoding.UTF8); + } + + [HttpGet("[action]/{id}")] + public async Task Library(string id, string appKey) + { + var hash = CalculateMd5Hash(appKey); + var uri = + $"https://coderr.io/client/{id}/configure/{hash}"; + var client = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + try + { + var html = await client.GetStringAsync(uri); + + var url = _baseConfiguration.Value.BaseUrl.ToString() + .Replace("https://app.", "https://report.") + .Replace("https://lobby.", "https://report.") + .Replace("http://live-test-lobby", "http://live-test-receiver"); + + return Content(html + //.Replace("yourAppKey", appInfo.AppKey) + //.Replace("yourSharedSecret", appInfo.SharedSecret) + .Replace("http://yourServer/coderr/", url) + .Replace("\"/documentation/", "\"https://coderr.io/documentation/"), "text/plain"); + } + catch (HttpRequestException) + { + return Content( + @"Cannot reach coderr.io, read the documentation to see how to configure Coderr or contact help@coderr.io.
+
+AppKey: yourAppKey
+Shared secret: yourSharedSecret"); + } + + } + + private string CalculateMd5Hash(string input) + { + var md5 = MD5.Create(); + var inputBytes = Encoding.ASCII.GetBytes(input); + var hash = md5.ComputeHash(inputBytes); + var sb = new StringBuilder(); + for (var i = 0; i < hash.Length; i++) + sb.Append(hash[i].ToString("x2")); + return sb.ToString(); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Controllers/PushController.cs b/src/Server/Coderr.Server.WebSite/Controllers/PushController.cs new file mode 100644 index 00000000..e39d553d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/PushController.cs @@ -0,0 +1,50 @@ +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Infrastructure.Configuration; +using Coderr.Server.ReportAnalyzer.UserNotifications; +using Coderr.Server.WebPush.Model; +using Coderr.Server.WebPush.Util; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Controllers +{ + [Produces("application/json")] + [Route("[controller]")] + [ApiController] + public class PushController : ControllerBase + { + private readonly ConfigurationStore _configurationStore; + private readonly IConfiguration _config; + private readonly IConfiguration _baseConfig; + + public PushController(IConfiguration config, ConfigurationStore configurationStore, IConfiguration baseConfig) + { + _configurationStore = configurationStore; + _baseConfig = baseConfig; + _config = config; + } + + + [HttpGet] + [Route("vapidpublickey")] + [Authorize] + public ActionResult VapidPublicKey() + { + if (!string.IsNullOrEmpty(_config.Value.PrivateKey)) + { + return Ok(_config.Value.PublicKey); + } + + var subject = ServerConfig.Instance.IsLive + ? VapidDetails.LiveSubject + : $"mailto:{_baseConfig.Value.SupportEmail}"; + var pair = VapidHelper.GenerateVapidKeys(subject); + _config.Value.PrivateKey = pair.PrivateKey; + _config.Value.PublicKey = pair.PublicKey; + _configurationStore.Store(_config.Value); + + return Ok(_config.Value.PublicKey); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Controllers/ReportReceiverController.cs b/src/Server/Coderr.Server.WebSite/Controllers/ReportReceiverController.cs new file mode 100644 index 00000000..d7ce65fd --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Controllers/ReportReceiverController.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Authentication; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Abstractions.Reports; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.App.Modules.Whitelists; +using Coderr.Server.ReportAnalyzer.Inbound; +using Coderr.Server.WebSite.Infrastructure.Adapters; +using Coderr.Server.WebSite.Models; +using DotNetCqs.Queues; +using Griffin.Data; +using Griffin.Net.Protocols.Http; +using log4net; +using Microsoft.AspNetCore.Mvc; + +namespace Coderr.Server.WebSite.Controllers +{ + //[EnableCors("CorsPolicy")] + public class ReportReceiverController : Controller + { + private const int CompressedReportSizeLimit = 1000000; + private readonly ILog _logger = LogManager.GetLogger(typeof(ReportReceiverController)); + private readonly IMessageQueue _messageQueue; + private readonly IAdoNetUnitOfWork _unitOfWork; + private readonly IConfiguration _reportConfig; + private readonly IWhitelistService _whitelistService; + + public ReportReceiverController(IMessageQueueProvider queueProvider, IAdoNetUnitOfWork unitOfWork, + IConfiguration reportConfig, IWhitelistService whitelistService) + { + _unitOfWork = unitOfWork; + _reportConfig = reportConfig; + _whitelistService = whitelistService; + _messageQueue = queueProvider.Open("ErrorReports"); + } + + [HttpGet] + [Route("receiver/report/")] + public IActionResult Index() + { + return Content("Hello world", "text/plain"); + } + + [HttpPost] + [Route("receiver/report/{appKey}")] + public async Task Post(string appKey, string sig) + { + var contentLength = Request.ContentLength; + if (contentLength > CompressedReportSizeLimit) + return await KillLargeReportAsync(appKey); + if (contentLength == null || contentLength < 1) + return BadRequest("Content required."); + var remoteIp = Request.HttpContext.Connection.RemoteIpAddress; + + _logger.Debug($"Report from {remoteIp} for {appKey}"); + + // Sig may be null for web applications + // as I don't know how to protect the secretKey in web applications + if (sig == null) + { + if (!await _whitelistService.Validate(appKey, remoteIp)) + return BadRequest($"Must sign error report with the sharedSecret or add ip/domain to the whitelist."); + } + + try + { + var buffer = new byte[contentLength.Value]; + var bytesRead = 0; + while (bytesRead < contentLength.Value) + { + bytesRead += await Request.Body.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead); + } + + var config = new ReportConfigWrapper(_reportConfig.Value); + var handler = new SaveReportHandler(_messageQueue, _unitOfWork, config); + var principal = CreateReporterPrincipal(); + + await handler.BuildReportAsync(principal, appKey, sig, remoteIp.ToString(), buffer); + return NoContent(); + } + catch (InvalidCredentialException) + { + return BadRequest(new ErrorMessage("INVALID_APP_KEY")); + } + catch (HttpException ex) + { + _logger.InfoFormat(ex.Message); + return new ContentResult + { + Content = ex.Message, + StatusCode = ex.HttpCode, + ContentType = "text/plain" + }; + } + catch (Exception exception) + { + _logger.Error( + "Failed to handle request from " + appKey + " / " + Request.HttpContext.Connection.RemoteIpAddress, + exception); + return new ContentResult + { + Content = exception.Message, + StatusCode = (int)HttpStatusCode.InternalServerError, + ContentType = "text/plain" + }; + } + } + + + internal static ClaimsPrincipal CreateReporterPrincipal() + { + var principal = new ClaimsPrincipal(new ClaimsIdentity(new List + { + new Claim(ClaimTypes.Name, "ReportReceiver"), + new Claim(ClaimTypes.NameIdentifier, "0"), + new Claim(ClaimTypes.Role, CoderrRoles.System) + }, "AppKey")); + return principal; + } + + private Task KillLargeReportAsync(string appKey) + { + _logger.Error(appKey + "Too large report: " + Request.ContentLength + " from " + + Request.HttpContext.Connection.RemoteIpAddress); + //TODO: notify + return Task.FromResult(NoContent()); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Dockerfile_Linux b/src/Server/Coderr.Server.WebSite/Dockerfile_Linux new file mode 100644 index 00000000..3ed7c62d --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Dockerfile_Linux @@ -0,0 +1,35 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-stretch-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:2.2-stretch AS build +WORKDIR /src +COPY ["Coderr.Server.Web/Coderr.Server.Web.csproj", "Coderr.Server.Web/"] +COPY ["Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj", "Coderr.Server.Abstractions/"] +COPY ["Coderr.Server.Api/Coderr.Server.Api.csproj", "Coderr.Server.Api/"] +COPY ["Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj", "Coderr.Server.Infrastructure/"] +COPY ["Coderr.Server.App/Coderr.Server.App.csproj", "Coderr.Server.App/"] +COPY ["Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj", "Coderr.Server.ReportAnalyzer.Abstractions/"] +COPY ["Coderr.Server.Domain/Coderr.Server.Domain.csproj", "Coderr.Server.Domain/"] +COPY ["Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj", "Coderr.Server.ReportAnalyzer/"] +COPY ["Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj", "Coderr.Server.SqlServer/"] +RUN dotnet restore "Coderr.Server.Web/Coderr.Server.Web.csproj" +COPY . . +WORKDIR "/src/Coderr.Server.Web" +RUN dotnet build "Coderr.Server.Web.csproj" -c Release -o /app/build + +WORKDIR /src +COPY Coderr.Server.Web/wwwroot Coderr.Server.Web/wwwroot + + +WORKDIR "/src/Coderr.Server.Web" + +FROM build AS publish +RUN dotnet publish "Coderr.Server.Web.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Coderr.Server.Web.dll"] diff --git a/src/Server/Coderr.Server.WebSite/Dockerfile_Windows b/src/Server/Coderr.Server.WebSite/Dockerfile_Windows new file mode 100644 index 00000000..e71968b1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Dockerfile_Windows @@ -0,0 +1,40 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +#Depending on the operating system of the host machines(s) that will build or run the containers, the image specified in the FROM statement may need to be changed. +#For more information, please see https://aka.ms/containercompat + +FROM mcr.microsoft.com/dotnet/core/aspnet:2.2-nanoserver-1809 AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:2.2-nanoserver-1809 AS build +WORKDIR /src +COPY ["Coderr.Server.Web/Coderr.Server.Web.csproj", "Coderr.Server.Web/"] +COPY ["Coderr.Server.Abstractions/Coderr.Server.Abstractions.csproj", "Coderr.Server.Abstractions/"] +COPY ["Coderr.Server.Api/Coderr.Server.Api.csproj", "Coderr.Server.Api/"] +COPY ["Coderr.Server.Infrastructure/Coderr.Server.Infrastructure.csproj", "Coderr.Server.Infrastructure/"] +COPY ["Coderr.Server.App/Coderr.Server.App.csproj", "Coderr.Server.App/"] +COPY ["Coderr.Server.ReportAnalyzer.Abstractions/Coderr.Server.ReportAnalyzer.Abstractions.csproj", "Coderr.Server.ReportAnalyzer.Abstractions/"] +COPY ["Coderr.Server.Domain/Coderr.Server.Domain.csproj", "Coderr.Server.Domain/"] +COPY ["Coderr.Server.ReportAnalyzer/Coderr.Server.ReportAnalyzer.csproj", "Coderr.Server.ReportAnalyzer/"] +COPY ["Coderr.Server.SqlServer/Coderr.Server.SqlServer.csproj", "Coderr.Server.SqlServer/"] + +RUN dotnet restore "Coderr.Server.Web/Coderr.Server.Web.csproj" + +COPY . . +WORKDIR "/src/Coderr.Server.Web" +RUN dotnet build "Coderr.Server.Web.csproj" -c Release -o /app/build + +WORKDIR /src + +COPY Coderr.Server.Web/wwwroot Coderr.Server.Web/wwwroot + +WORKDIR "/src/Coderr.Server.Web" + +FROM build AS publish +RUN dotnet publish "Coderr.Server.Web.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Coderr.Server.Web.dll"] diff --git a/src/Server/Coderr.Server.WebSite/Hubs/CoderrHub.cs b/src/Server/Coderr.Server.WebSite/Hubs/CoderrHub.cs new file mode 100644 index 00000000..c55884e0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Hubs/CoderrHub.cs @@ -0,0 +1,9 @@ +using System.Threading.Tasks; + +namespace Coderr.Server.WebSite.Hubs +{ + public interface CoderrHub + { + Task OnEvent(HubEvent evt); + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Hubs/HubEntity.cs b/src/Server/Coderr.Server.WebSite/Hubs/HubEntity.cs new file mode 100644 index 00000000..f4c74b1f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Hubs/HubEntity.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Coderr.Server.WebSite.Hubs +{ + public class HubEntity + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public bool IsEvent { get; set; } + + } +} diff --git a/src/Server/Coderr.Server.WebSite/Hubs/HubEvent.cs b/src/Server/Coderr.Server.WebSite/Hubs/HubEvent.cs new file mode 100644 index 00000000..75716736 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Hubs/HubEvent.cs @@ -0,0 +1,13 @@ +using System; +using System.Linq; +using Newtonsoft.Json; + +namespace Coderr.Server.WebSite.Hubs +{ + public class HubEvent + { + public string TypeName { get; set; } + public Guid CorrelationId { get; set; } + public object Body { get; set; } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Hubs/HubSession.cs b/src/Server/Coderr.Server.WebSite/Hubs/HubSession.cs new file mode 100644 index 00000000..720fa749 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Hubs/HubSession.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using DotNetCqs; +using DotNetCqs.Queues; + +namespace Coderr.Server.WebSite.Hubs +{ + public class HubSession : IMessageQueueSession + { + private readonly WebSocketHub _webSocketHub; + private List _messages = new List(); + private ClaimsPrincipal _principal; + + + public HubSession(WebSocketHub webSocketHub) + { + _webSocketHub = webSocketHub; + } + + public void Dispose() + { + + } + + public Task Dequeue(TimeSpan suggestedWaitPeriod) + { + throw new NotSupportedException(); + } + + public Task DequeueWithCredentials(TimeSpan suggestedWaitPeriod) + { + throw new NotSupportedException(); + } + + public Task EnqueueAsync(ClaimsPrincipal principal, IReadOnlyCollection messages) + { + AssignPrincipal(principal); + foreach (var message in messages) + { + _messages.Add(message); + } + + return Task.CompletedTask; + } + + private void AssignPrincipal(ClaimsPrincipal principal) + { + if (_principal != null && _principal.Identity.Name != principal.Identity.Name) + { + throw new InvalidOperationException("Expected same principal"); + } + + _principal = principal; + } + + public Task EnqueueAsync(IReadOnlyCollection messages) + { + foreach (var message in messages) + { + _messages.Add(message); + } + return Task.CompletedTask; + + } + + public Task EnqueueAsync(ClaimsPrincipal principal, Message message) + { + AssignPrincipal(principal); + + _messages.Add(message); + return Task.CompletedTask; + } + + public Task EnqueueAsync(Message message) + { + _messages.Add(message); + return Task.CompletedTask; + + } + + public async Task SaveChanges() + { + await _webSocketHub.Send(_principal, _messages); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Hubs/HubSubscribe.cs b/src/Server/Coderr.Server.WebSite/Hubs/HubSubscribe.cs new file mode 100644 index 00000000..da5c9508 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Hubs/HubSubscribe.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Coderr.Server.WebSite.Hubs +{ + public class HubSubscribe + { + public string MessageName { get; set; } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Hubs/WebSocketHub.cs b/src/Server/Coderr.Server.WebSite/Hubs/WebSocketHub.cs new file mode 100644 index 00000000..833d25d0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Hubs/WebSocketHub.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.Api; +using Coderr.Server.Infrastructure.Messaging; +using Coderr.Server.WebSite.Infrastructure.Cqs; +using DotNetCqs; +using DotNetCqs.Queues; +using Microsoft.AspNetCore.SignalR; + +namespace Coderr.Server.WebSite.Hubs +{ + public class WebSocketHub : Hub, IMessageQueue + { + public WebSocketHub(IRouteRegistrar registrar) + { + registrar.RegisterAppQueue(this); + registrar.RegisterReportQueue(this); + } + + public IMessageQueueSession BeginSession() + { + return new HubSession(this); + } + + public override Task OnConnectedAsync() + { + Clients.Caller.OnEvent(new HubEvent() + { + TypeName = "HelloWorld", + Body = "Hello the world!" + }); + return base.OnConnectedAsync(); + } + + public string Name { get; } = "WebSocket"; + + public async Task Send(ClaimsPrincipal principal, IReadOnlyList messages) + { + var eventMessages = messages + .Where(x => x.GetType().GetCustomAttribute() == null && + !RegisterCqsServices.IsQuery(x)) + .ToList(); + if (principal == null) + { + return; + } + + var sender = Clients.All; + + foreach (var message in eventMessages) + { + await sender.OnEvent(new HubEvent + { + CorrelationId = message.CorrelationId, + Body = message.Body, + TypeName = message.Body.GetType().Name + }); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/AuthenticationSchemeProvider2.cs b/src/Server/Coderr.Server.WebSite/Identity/AuthenticationSchemeProvider2.cs new file mode 100644 index 00000000..aa77a83a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/AuthenticationSchemeProvider2.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; + +namespace Coderr.Server.WebSite.Identity +{ + public class AuthenticationSchemeProvider2 : IAuthenticationSchemeProvider + { + private List _schemes = new List(); + public void AddScheme(AuthenticationScheme scheme) + { + _schemes.Add(scheme); + } + + public Task> GetAllSchemesAsync() + { + return Task.FromResult>(_schemes); + } + + public Task GetDefaultAuthenticateSchemeAsync() + { + return Task.FromResult(_schemes.FirstOrDefault()); + } + + public Task GetDefaultChallengeSchemeAsync() + { + return Task.FromResult(_schemes.FirstOrDefault()); + } + + public Task GetDefaultForbidSchemeAsync() + { + return Task.FromResult(_schemes.FirstOrDefault()); + } + + public Task GetDefaultSignInSchemeAsync() + { + return Task.FromResult(_schemes.FirstOrDefault()); + } + + public Task GetDefaultSignOutSchemeAsync() + { + return Task.FromResult(_schemes.FirstOrDefault()); + } + + public Task> GetRequestHandlerSchemesAsync() + { + return Task.FromResult>(_schemes); + } + + public Task GetSchemeAsync(string name) + { + return Task.FromResult(_schemes.FirstOrDefault(x=>x.Name==name)); + } + + public void RemoveScheme(string name) + { + _schemes.RemoveAll(x => x.Name == name); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/DbTransactionExtensions.cs b/src/Server/Coderr.Server.WebSite/Identity/DbTransactionExtensions.cs new file mode 100644 index 00000000..8723e0db --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/DbTransactionExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Linq; +using System.Threading.Tasks; + +namespace Coderr.Server.WebSite.Identity +{ + public static class DbTransactionExtensions + { + public static DbCommand CreateDbCommand(this IDbTransaction transaction) + { + var cmd = ((DbTransaction)transaction).Connection.CreateCommand(); + cmd.Transaction = (DbTransaction)transaction; + return cmd; + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Identity/LookupNormalizer.cs b/src/Server/Coderr.Server.WebSite/Identity/LookupNormalizer.cs new file mode 100644 index 00000000..450e4418 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/LookupNormalizer.cs @@ -0,0 +1,17 @@ +using Microsoft.AspNetCore.Identity; + +namespace Coderr.Server.WebSite.Identity +{ + public class LookupNormalizer : ILookupNormalizer + { + public string NormalizeName(string name) + { + return name.ToUpperInvariant(); + } + + public string NormalizeEmail(string email) + { + return email.ToUpperInvariant(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/MyIdentityErrorDescriber.cs b/src/Server/Coderr.Server.WebSite/Identity/MyIdentityErrorDescriber.cs new file mode 100644 index 00000000..6b751f6f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/MyIdentityErrorDescriber.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Identity; + +namespace Coderr.Server.WebSite.Identity +{ + public class MyIdentityErrorDescriber : IdentityErrorDescriber + { + + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/MySigninManager.cs b/src/Server/Coderr.Server.WebSite/Identity/MySigninManager.cs new file mode 100644 index 00000000..67fbc410 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/MySigninManager.cs @@ -0,0 +1,16 @@ +using Coderr.Server.WebSite.Models; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Coderr.Server.WebSite.Identity +{ + public class MySigninManager : SignInManager + { + public MySigninManager(UserManager userManager, IHttpContextAccessor contextAccessor, IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor, ILogger> logger, IAuthenticationSchemeProvider schemes, IUserConfirmation confirmation) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/MyUserManager.cs b/src/Server/Coderr.Server.WebSite/Identity/MyUserManager.cs new file mode 100644 index 00000000..9fe3179c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/MyUserManager.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using Coderr.Server.WebSite.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Coderr.Server.WebSite.Identity +{ + public class MyUserManager : UserManager + { + public MyUserManager(IUserStore store, IOptions optionsAccessor, IPasswordHasher passwordHasher, IEnumerable> userValidators, IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer, IdentityErrorDescriber errors, IServiceProvider services, ILogger> logger) : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger) + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/UserClaimStore.cs b/src/Server/Coderr.Server.WebSite/Identity/UserClaimStore.cs new file mode 100644 index 00000000..6476ec23 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/UserClaimStore.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Coderr.Server.WebSite.Models; +using Microsoft.AspNetCore.Identity; + +namespace Coderr.Server.WebSite.Identity +{ + public class UserClaimStore : IUserClaimStore + { + public void Dispose() + { + throw new NotImplementedException(); + } + + public Task GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task CreateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task UpdateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetClaimsAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task AddClaimsAsync(ApplicationUser user, IEnumerable claims, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task ReplaceClaimAsync(ApplicationUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task RemoveClaimsAsync(ApplicationUser user, IEnumerable claims, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/UserClaimsFactory.cs b/src/Server/Coderr.Server.WebSite/Identity/UserClaimsFactory.cs new file mode 100644 index 00000000..2e5f8ec8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/UserClaimsFactory.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading.Tasks; +using Coderr.Server.WebSite.Models; +using Microsoft.AspNetCore.Identity; + +namespace Coderr.Server.WebSite.Identity +{ + public class UserClaimsFactory : IUserClaimsPrincipalFactory + { + public Task CreateAsync(ApplicationUser user) + { + var identity = new ClaimsIdentity(new List() + { + new Claim(ClaimTypes.Name, user.UserName), + new Claim(ClaimTypes.NameIdentifier, user.Id), + new Claim(ClaimTypes.Email, user.Email) + }); + return Task.FromResult(new ClaimsPrincipal(identity)); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/UserConfirmation.cs b/src/Server/Coderr.Server.WebSite/Identity/UserConfirmation.cs new file mode 100644 index 00000000..c529d9aa --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/UserConfirmation.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using Coderr.Server.WebSite.Models; +using Microsoft.AspNetCore.Identity; + +namespace Coderr.Server.WebSite.Identity +{ + public class UserConfirmation : IUserConfirmation + { + public Task IsConfirmedAsync(UserManager manager, ApplicationUser user) + { + return Task.FromResult(user.EmailConfirmed); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Identity/UserStore.cs b/src/Server/Coderr.Server.WebSite/Identity/UserStore.cs new file mode 100644 index 00000000..358f7a13 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Identity/UserStore.cs @@ -0,0 +1,172 @@ +using System; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Coderr.Server.WebSite.Models; +using Griffin.Data; +using Microsoft.AspNetCore.Identity; + +namespace Coderr.Server.WebSite.Identity +{ + public class UserStore : IUserStore + { + private IAdoNetUnitOfWork _dbTransaction; + + public UserStore(IAdoNetUnitOfWork dbTransaction) + { + _dbTransaction = dbTransaction; + } + + public void Dispose() + { + } + + public async Task GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken) + { + using (var cmd = _dbTransaction.CreateDbCommand()) + { + if (string.IsNullOrEmpty(user.Email)) + { + cmd.CommandText = "SELECT Id FROM Accounts WHERE UserName = @userName"; + cmd.AddParameter("userName", user.UserName); + } + else + { + cmd.CommandText = "SELECT Id FROM Accounts WHERE Email = @email"; + cmd.AddParameter("email", user.Email); + } + var result = await cmd.ExecuteScalarAsync(cancellationToken); + return result is DBNull ? null : result.ToString(); + } + } + + public async Task GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + using (var cmd = _dbTransaction.CreateDbCommand()) + { + if (!string.IsNullOrEmpty(user.Id)) + { + cmd.CommandText = "SELECT UserName FROM Accounts WHERE Id = @id"; + cmd.AddParameter("id", user.Id); + } + else + { + cmd.CommandText = "SELECT UserName FROM Accounts WHERE Email = @email"; + cmd.AddParameter("email", user.Email); + } + var result = await cmd.ExecuteScalarAsync(cancellationToken); + return result is DBNull ? null : result.ToString(); + } + } + + public async Task SetUserNameAsync(ApplicationUser user, string userName, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(user.Id)) + throw new ArgumentNullException("user.Id"); + + using (var cmd1 = _dbTransaction.CreateDbCommand()) + { + cmd1.CommandText = "UPDATE Users SET UserName=@userName WHERE AccountId = @id"; + cmd1.AddParameter("id", user.Id); + await cmd1.ExecuteScalarAsync(cancellationToken); + } + + using (var cmd2 = _dbTransaction.CreateDbCommand()) + { + cmd2.CommandText = "UPDATE Accounts SET UserName=@userName WHERE Id = @id"; + cmd2.AddParameter("id", user.Id); + await cmd2.ExecuteScalarAsync(cancellationToken); + } + } + + public async Task GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return (await GetUserNameAsync(user, cancellationToken))?.ToUpper(); + } + + public Task SetNormalizedUserNameAsync(ApplicationUser user, string normalizedName, CancellationToken cancellationToken) + { + return Task.CompletedTask; + } + + public async Task CreateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + using (var cmd = (DbCommand)_dbTransaction.CreateDbCommand()) + { + cmd.CommandText = "INSERT INTO Users (AccountId, UserName, EmailAddress, HashedPassword) VALUES(@id, @userName, @email, @passwordHash)"; + cmd.AddParameter("id", int.Parse(user.Id)); + cmd.AddParameter("userName", user.UserName); + cmd.AddParameter("email", user.Email); + cmd.AddParameter("passwordHash", user.PasswordHash); + //TODO: Not using our salt. + await cmd.ExecuteNonQueryAsync(cancellationToken); + } + + return new IdentityResult {Errors = { }}; + + //TODO: User + } + + public Task UpdateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task DeleteAsync(ApplicationUser user, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public async Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + using (var cmd = _dbTransaction.CreateDbCommand()) + { + cmd.CommandText = "SELECT Id, UserName, Email, HashedPassword, LoginAttempts FROM Accounts WHERE Id = @id"; + cmd.AddParameter("id", userId); + using (var reader = await cmd.ExecuteReaderAsync(cancellationToken)) + { + if (!await reader.ReadAsync(cancellationToken)) + return null; + + var user = new ApplicationUser + { + Id = reader.GetInt32(0).ToString(), + UserName = reader.GetString(1), + NormalizedUserName = reader.GetString(1).ToUpper(), + Email = reader.GetString(2), + NormalizedEmail = reader.GetString(2), + PasswordHash = reader.GetString(3), + AccessFailedCount = reader.GetInt32(4) + }; + return user; + } + } + } + + public async Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + using (var cmd = _dbTransaction.CreateDbCommand()) + { + cmd.CommandText = "SELECT Id, UserName, Email, HashedPassword, LoginAttempts FROM Accounts WHERE UserName = @userName"; + cmd.AddParameter("userName", normalizedUserName); + using (var reader = await cmd.ExecuteReaderAsync(cancellationToken)) + { + if (!await reader.ReadAsync(cancellationToken)) + return null; + + var user = new ApplicationUser + { + Id = reader.GetInt32(0).ToString(), + UserName = reader.GetString(1), + NormalizedUserName = reader.GetString(1).ToUpper(), + Email = reader.GetString(2), + NormalizedEmail = reader.GetString(2), + PasswordHash = reader.GetString(3), + AccessFailedCount = reader.GetInt32(4) + }; + return user; + } + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ConfigurationSectionWrapper.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ConfigurationSectionWrapper.cs new file mode 100644 index 00000000..f79158de --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ConfigurationSectionWrapper.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Coderr.Server.Abstractions.Boot; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters +{ + public class ConfigurationSectionWrapper : IConfigurationSection + { + private readonly Microsoft.Extensions.Configuration.IConfigurationSection _section; + + public ConfigurationSectionWrapper(Microsoft.Extensions.Configuration.IConfigurationSection section) + { + _section = section; + } + + public string this[string name] => _section[name]; + + public IEnumerable GetChildren() + { + return _section.GetChildren().Select(x => new ConfigurationSectionWrapper(x)); + } + + public string Value => _section.Value; + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ConfigurationWrapper.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ConfigurationWrapper.cs new file mode 100644 index 00000000..04d14793 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ConfigurationWrapper.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using IConfiguration = Coderr.Server.Abstractions.Boot.IConfiguration; +using IConfigurationSection = Coderr.Server.Abstractions.Boot.IConfigurationSection; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters +{ + public class ConfigurationWrapper : IConfiguration + { + private readonly Microsoft.Extensions.Configuration.IConfiguration _config; + + public ConfigurationWrapper(Microsoft.Extensions.Configuration.IConfiguration config) + { + _config = config; + } + + public string this[string name] => _config[name]; + + public IEnumerable GetChildren() + { + return _config.GetChildren().Select(x => new ConfigurationSectionWrapper(x)); + } + + public IConfigurationSection GetSection(string name) + { + var section = _config.GetSection(name); + return section == null + ? null + : new ConfigurationSectionWrapper(section); + } + + public string GetConnectionString(string name) + { + return _config.GetConnectionString(name); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/CqsObjectMapperConfigurationContext.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/CqsObjectMapperConfigurationContext.cs new file mode 100644 index 00000000..b6815115 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/CqsObjectMapperConfigurationContext.cs @@ -0,0 +1,31 @@ +using System; +using System.Reflection; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.WebSite.Infrastructure.Cqs; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters +{ + public class CqsObjectMapperConfigurationContext : ConfigurationContext + { + private readonly CqsObjectMapper _cqsObjectMapper; + + public CqsObjectMapperConfigurationContext(CqsObjectMapper cqsObjectMapper, IServiceCollection services, Func serviceProvider) : base(services, serviceProvider) + { + _cqsObjectMapper = cqsObjectMapper; + } + + public override void RegisterMessageTypes(Assembly assembly) + { + _cqsObjectMapper.ScanAssembly(assembly); + } + + public override ConfigurationContext Clone(IServiceCollection serviceCollection) + { + return new CqsObjectMapperConfigurationContext(_cqsObjectMapper, serviceCollection, ServiceProvider) + { + Configuration = Configuration, ConnectionFactory = ConnectionFactory + }; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/DependencyInjectionAdapter.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/DependencyInjectionAdapter.cs new file mode 100644 index 00000000..2ad921c4 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/DependencyInjectionAdapter.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Griffin.Container; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters +{ + public class DependencyInjectionAdapter : IContainer + { + private readonly IServiceProvider _serviceProvider; + + public DependencyInjectionAdapter(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public TService Resolve() + { + return _serviceProvider.GetService(); + } + + public object Resolve(Type service) + { + return _serviceProvider.GetService(service); + } + + public IEnumerable ResolveAll() + { + return _serviceProvider.GetServices(); + } + + public IEnumerable ResolveAll(Type service) + { + return _serviceProvider.GetServices(service); + } + + public IContainerScope CreateScope() + { + var scope = _serviceProvider.CreateScope(); + return new ServiceScopeWrapper(scope); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/HandlerScopeWrapper.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/HandlerScopeWrapper.cs new file mode 100644 index 00000000..692df64b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/HandlerScopeWrapper.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using DotNetCqs.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters +{ + public class HandlerScopeWrapper : IHandlerScope + { + private readonly IServiceProvider _serviceProvider; + + public HandlerScopeWrapper(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public void Dispose() + { + var disp = _serviceProvider as IDisposable; + disp?.Dispose(); + } + + public IEnumerable Create(Type messageHandlerServiceType) + { + return _serviceProvider.GetServices(messageHandlerServiceType); + } + + public IEnumerable ResolveDependency() + { + return _serviceProvider.GetServices(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/FakeLogScope.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/FakeLogScope.cs new file mode 100644 index 00000000..4123ffb2 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/FakeLogScope.cs @@ -0,0 +1,18 @@ +using System; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters.Logging +{ + public class FakeLogScope : IDisposable + { + private readonly object _state; + + public FakeLogScope(object state) + { + _state = state; + } + + public void Dispose() + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/MicrosoftLogAdapter.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/MicrosoftLogAdapter.cs new file mode 100644 index 00000000..fd02bcb3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/MicrosoftLogAdapter.cs @@ -0,0 +1,52 @@ +using System; +using System.Diagnostics; +using log4net; +using Microsoft.Extensions.Logging; +using ILogger = Microsoft.Extensions.Logging.ILogger; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters.Logging +{ + public class MicrosoftLogAdapter : ILogger + { + private ILog _logger; + + public MicrosoftLogAdapter(ILog logger) + { + _logger = logger; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + switch (logLevel) + { + case LogLevel.Trace: + case LogLevel.Debug: + _logger.Debug(formatter(state, exception)); + break; + case LogLevel.Information: + _logger.Info(formatter(state, exception)); + break; + case LogLevel.Warning: + _logger.Warn(formatter(state, exception)); + break; + case LogLevel.Error: + case LogLevel.Critical: + _logger.Error(formatter(state, exception)); + break; + default: + Debugger.Break(); + break; + } + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public IDisposable BeginScope(TState state) + { + return new FakeLogScope(state); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/MicrosoftLogFactoryAdapter.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/MicrosoftLogFactoryAdapter.cs new file mode 100644 index 00000000..c6df14ff --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/Logging/MicrosoftLogFactoryAdapter.cs @@ -0,0 +1,24 @@ +using System; +using log4net; +using Microsoft.Extensions.Logging; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters.Logging +{ + public class MicrosoftLogFactoryAdapter : ILoggerFactory + { + public void Dispose() + { + } + + public ILogger CreateLogger(string categoryName) + { + var logger = LogManager.GetLogger(typeof(MicrosoftLogAdapter)); + return new MicrosoftLogAdapter(logger); + } + + public void AddProvider(ILoggerProvider provider) + { + + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ReportConfigWrapper.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ReportConfigWrapper.cs new file mode 100644 index 00000000..f3c299fc --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ReportConfigWrapper.cs @@ -0,0 +1,17 @@ +using Coderr.Server.Abstractions.Reports; +using Coderr.Server.ReportAnalyzer.Abstractions.ErrorReports; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters +{ + public class ReportConfigWrapper : IReportConfig + { + private readonly ReportConfig _inner; + + public ReportConfigWrapper(ReportConfig inner) + { + _inner = inner; + } + + public int MaxReportJsonSize => _inner.MaxReportJsonSize; + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ServiceScopeWrapper.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ServiceScopeWrapper.cs new file mode 100644 index 00000000..b3845b66 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Adapters/ServiceScopeWrapper.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using Griffin.Container; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.WebSite.Infrastructure.Adapters +{ + public class ServiceScopeWrapper : IContainerScope + { + private readonly IServiceScope _scope; + + public ServiceScopeWrapper(IServiceScope scope) + { + _scope = scope ?? throw new ArgumentNullException(nameof(scope)); + } + + public void Dispose() + { + _scope.Dispose(); + } + + public TService Resolve() + { + return _scope.ServiceProvider.GetService(); + } + + public object Resolve(Type service) + { + return _scope.ServiceProvider.GetService(service); + } + + public IEnumerable ResolveAll() + { + return _scope.ServiceProvider.GetServices(); + } + + public IEnumerable ResolveAll(Type service) + { + return _scope.ServiceProvider.GetServices(service); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/ApiKeyAuthentication/ApiKeyAuthOptions.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/ApiKeyAuthentication/ApiKeyAuthOptions.cs new file mode 100644 index 00000000..e02183a1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/ApiKeyAuthentication/ApiKeyAuthOptions.cs @@ -0,0 +1,14 @@ +using System; +using System.Data; +using Microsoft.AspNetCore.Authentication; + +namespace Coderr.Server.WebSite.Infrastructure.ApiKeyAuthentication +{ + public class ApiKeyAuthOptions : AuthenticationSchemeOptions + { + public const string DefaultSchemeName = "ApiKey"; + public string AuthenticationScheme = DefaultSchemeName; + + public Func OpenDb; + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/ApiKeyAuthentication/ApiKeyAuthenticator.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/ApiKeyAuthentication/ApiKeyAuthenticator.cs new file mode 100644 index 00000000..becd3b29 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/ApiKeyAuthentication/ApiKeyAuthenticator.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.App.Core.ApiKeys; +using Coderr.Server.SqlServer.Core.ApiKeys; +using Griffin.Data; +using log4net; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Coderr.Server.WebSite.Infrastructure.ApiKeyAuthentication +{ + public class ApiKeyAuthenticator : AuthenticationHandler + { + private readonly IOptionsMonitor _options; + private ILog _logger = LogManager.GetLogger(typeof(ApiKeyAuthenticator)); + + public bool AllowMultiple => true; + + public ApiKeyAuthenticator(IOptionsMonitor options, ILoggerFactory logger, + UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) + { + _options = options; + } + + protected override async Task HandleAuthenticateAsync() + { + var claimsPrincipal = Context.User; + if (claimsPrincipal != null && claimsPrincipal.Identity.IsAuthenticated) + { + _logger.Debug("Already authenticated."); + return AuthenticateResult.NoResult(); + } + + var ticket = await CreateTicket(Request, _options.CurrentValue); + return AuthenticateResult.Success(ticket); + } + + public static async Task CreateTicket(HttpRequest request, ApiKeyAuthOptions options) + { + var apiKeyHeader = request.Headers["X-Api-Key"]; + var signatureHeader = request.Headers["X-Api-Signature"]; + + if (apiKeyHeader.Count == 0 || + signatureHeader.Count == 0) + return null; + var apiKey = apiKeyHeader[0]; + + var claims = new List() + { + new Claim(ClaimTypes.NameIdentifier, apiKey), + new Claim(ClaimTypes.Name, "ApiKey"), + new Claim(ClaimTypes.Role, CoderrRoles.System) + }; + var identity = new ClaimsIdentity(claims, options.AuthenticationScheme); + var claimsPrincipal = new ClaimsPrincipal(identity); + + + using (var con = options.OpenDb()) + { + using (var uow = new AdoNetUnitOfWork(con, false)) + { + var repos = new ApiKeyRepository(uow); + ApiKey key; + try + { + key = await repos.GetByKeyAsync(apiKey); + } + catch (EntityNotFoundException) + { + return null; + } + + request.EnableBuffering(); + + var buf = new byte[request.ContentLength ?? 0]; + var bytesRead = await request.Body.ReadAsync(buf, 0, buf.Length); + request.Body.Position = 0; + if (!key.ValidateSignature(signatureHeader[0], buf)) + { + return null; + } + + if (key.Claims != null) + { + identity.AddClaims(key.Claims); + } + + uow.SaveChanges(); + } + } + + return new AuthenticationTicket(claimsPrincipal, options.AuthenticationScheme); + } + } + +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/AppModuleStarter.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/AppModuleStarter.cs new file mode 100644 index 00000000..628e4584 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/AppModuleStarter.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.ReportAnalyzer.Boot; +using log4net; +using Microsoft.Extensions.DependencyInjection; +using ConfigurationContext = Coderr.Server.Abstractions.Boot.ConfigurationContext; +using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; +using StartContext = Coderr.Server.Abstractions.Boot.StartContext; + +namespace Coderr.Server.WebSite.Infrastructure +{ + public class AppModuleStarter + { + private readonly List _modules = new List(); + private readonly List _ignoredAppModules = new List(); + private ILog _logger = LogManager.GetLogger(typeof(ReportAnalyzerModuleStarter)); + + public AppModuleStarter(IConfiguration configuration) + { + foreach (var child in configuration.GetSection("DisabledModules:App").GetChildren()) + { + _ignoredAppModules.Add(child.Value); + } + + } + + public void RegisterModule(Type type) + { + var module = (IAppModule)Activator.CreateInstance(type); + _modules.Add(module); + } + + public void ScanAssemblies(string assemblyDirectory) + { + var files = Directory.GetFiles(assemblyDirectory, "*.dll"); + foreach (var fullPath in files) + { + var fileName = Path.GetFileName(fullPath); + if (!fileName.Contains("Coderr")) + continue; + + + var assembly = Assembly.LoadFrom(fullPath); + var types = assembly.GetTypes() + .Where(x => typeof(IAppModule).IsAssignableFrom(x) && !x.IsAbstract && !x.IsInterface) + .ToList(); + foreach (var type in types) + { + if (_ignoredAppModules.Any(x => x.Equals(type.Name, StringComparison.OrdinalIgnoreCase))) + continue; + + if (ServerConfig.Instance.IsModuleIgnored(type)) + continue; + + RegisterModule(type); + } + + + } + } + + public void Configure(ConfigurationContext context) + { + foreach (var module in _modules) + { + + var childServices = new ServiceCollection(); + var moduleContext = context.Clone(childServices); + + module.Configure(moduleContext); + + + foreach (var service in moduleContext.Services) + { + if (service.ImplementationType?.Name == "DeleteAbandonedSimilarities") + Debugger.Break(); + + //if (context.Services.Any(x => x.ImplementationType == service.ImplementationType)) + //{ + // throw new InvalidOperationException( + // $"Service {service.ImplementationType} has already been registered."); + //} + + context.Services.Add(service); + } + } + } + + public void Start(StartContext context) + { + foreach (var module in _modules) + { + module.Start(context); + } + } + + public void Stop() + { + foreach (var module in _modules) + { + module.Stop(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Boot/PrincipalWrapper.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Boot/PrincipalWrapper.cs new file mode 100644 index 00000000..ca5b3e3a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Boot/PrincipalWrapper.cs @@ -0,0 +1,45 @@ +using System; +using System.Security.Claims; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Abstractions.Security; +using log4net; +using Microsoft.AspNetCore.Http; + +namespace Coderr.Server.WebSite.Infrastructure.Boot +{ + [ContainerService] + public class PrincipalWrapper : IPrincipalAccessor + { + private readonly IHttpContextAccessor _httpContextAccessor; + private ClaimsPrincipal _principal; + private ILog _logger = LogManager.GetLogger(typeof(PrincipalWrapper)); + + public PrincipalWrapper(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + public ClaimsPrincipal Principal + { + get + { + if (_principal != null) + { + return _principal; + } + + if (_httpContextAccessor.HttpContext == null) + throw new InvalidOperationException("Principal was not assigned and the HttpContext is not available."); + + return _httpContextAccessor.HttpContext.User; + } + + set => _principal = value; + } + + public ClaimsPrincipal FindPrincipal() + { + return _principal ?? _httpContextAccessor.HttpContext?.User; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/CoderrStartup.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/CoderrStartup.cs new file mode 100644 index 00000000..8c3dc689 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/CoderrStartup.cs @@ -0,0 +1,258 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Security.Claims; +using System.Threading; +using Coderr.Client; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.Infrastructure; +using Coderr.Server.Infrastructure.Configuration.Database; +using Coderr.Server.Infrastructure.Messaging; +using Coderr.Server.ReportAnalyzer.Boot.Adapters; +using Coderr.Server.SqlServer; +using Coderr.Server.SqlServer.Migrations; +using Coderr.Server.SqlServer.Schema; +using Coderr.Server.WebSite.Controllers; +using Coderr.Server.WebSite.Infrastructure.Adapters; +using log4net; +using log4net.Appender; +using log4net.Repository.Hierarchy; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; +using RegisterExtensions = Coderr.Server.Abstractions.Boot.RegisterExtensions; + +namespace Coderr.Server.WebSite.Infrastructure +{ + public class CoderrStartup + { + private readonly IConfiguration _configuration; + private readonly ILog _logger = LogManager.GetLogger(typeof(Startup)); + private readonly QueueManager _queueManager = new QueueManager(); + private IHostApplicationLifetime _applicationLifetime; + private readonly AppModuleStarter _appModuleStarter; + private IServiceProvider _serviceProvider; + + public CoderrStartup(IConfiguration configuration) + { + _configuration = configuration; + ServerConfig.Instance.ServerType = _configuration.GetSection("AppSettings").GetValue("IsLive") + ? ServerType.Live + : ServerType.Premise; + HostConfig.Instance.InstallationPassword = _configuration["General:InstallationPassword"]; + + var configWrapper = new ConfigurationWrapper(_configuration); + + LoadHostConfiguration(_configuration); + + _appModuleStarter = new AppModuleStarter(_configuration); + _queueManager.Configure(configWrapper, OpenConnection); + _queueManager.ShutdownRequested += OnShutdownRequested; + + ValidateConfiguration(configuration); + ConfigureMigrations(); + } + + private void ValidateConfiguration(IConfiguration configuration) + { + var tools = new SqlServerTools(() => OpenConnection(CoderrClaims.SystemPrincipal)); + SetupTools.DbTools = tools; + var conString = configuration.GetConnectionString("Db"); + if (!string.IsNullOrEmpty(conString) && !conString.Contains("ReplaceMe") && tools.IsConfigurationComplete(conString)) + { + HostConfig.Instance.MarkAsConfigured(); + } + } + + public void Configure(IApplicationBuilder applicationBuilder, IHostApplicationLifetime applicationLifetime) + { + _applicationLifetime = applicationLifetime; + applicationLifetime.ApplicationStopping.Register(OnShutdown); + } + + public void ConfigureAuthentication(IApplicationBuilder app) + { + app.UseStatusCodePages(async x => + { + _logger.Error($"{x.HttpContext.Response.StatusCode}: {x.HttpContext.Request.GetDisplayUrl()}"); + x.HttpContext.Response.ContentType = "text/plain"; + + var erroMsg = "General Failure"; + var exceptionHandlerPathFeature = + x.HttpContext.Features.Get(); + if (exceptionHandlerPathFeature != null) + erroMsg = exceptionHandlerPathFeature.Error.Message; + + await x.HttpContext.Response.WriteAsync( + "Error " + + x.HttpContext.Response.StatusCode + " " + erroMsg); + }); + } + + public void ConfigureEnd(IApplicationBuilder applicationBuilder) + { + _serviceProvider = applicationBuilder.ApplicationServices; + HostConfig.Instance.Configured += (sender, args) => + { + var context = new StartContext { ServiceProvider = applicationBuilder.ApplicationServices }; + _appModuleStarter.Start(context); + _logger.Info("Coderr started successfully."); + }; + } + + public void BeginConfigureServices(IServiceCollection services) + { + if (!ServerConfig.Instance.IsLive) + UpgradeDatabaseSchema(); + } + + public void EndConfigureServices(IServiceCollection services) + { + _appModuleStarter.ScanAssemblies(AppDomain.CurrentDomain.BaseDirectory); + + services.AddSingleton(new ConfigurationWrapper(_configuration)); + services.AddSingleton(); + //services.AddScoped(); + + RegisterExtensions.RegisterContainerServices(services, Assembly.GetExecutingAssembly()); + //Coderr.Server.WebSite.Infrastructure.Modules.RegisterContainerServices() + //services.RegisterContainerServices(Assembly.GetExecutingAssembly()); + + RegisterConfigurationStores(services); + + var configContext = new CqsObjectMapperConfigurationContext(CqsController._cqsObjectMapper, services, () => _serviceProvider) + { + Configuration = new ConfigurationWrapper(_configuration), + ConnectionFactory = OpenConnection + }; + _appModuleStarter.Configure(configContext); + var k = services.Where(x => x.ServiceType.Name == "ITriggerRepository").ToList(); + _logger.Debug("All services is now running."); + } + + private void ConfigureMigrations() + { + var baseRunner = new MigrationRunner( + () => OpenConnection(CoderrClaims.SystemPrincipal), + "Coderr", + typeof(CoderrMigrationPointer).Namespace); + SqlServerTools.AddMigrationRunner(baseRunner); + } + + private void LoadHostConfiguration(IConfiguration configuration) + { + HostConfig.Instance.IsRunningInDocker = + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER")); + if (HostConfig.Instance.IsRunningInDocker) + { + HostConfig.Instance.ConnectionString = Environment.GetEnvironmentVariable("CODERR_CONNECTIONSTRING"); + + ((Logger)_logger.Logger).AddAppender(new ManagedColoredConsoleAppender()); + } + else + { + HostConfig.Instance.ConnectionString = configuration["ConnectionStrings:Db"]; + HostConfig.Instance.IsDemo = configuration.GetSection("General")?.GetValue("IsDemo") ?? + Debugger.IsAttached; + } + + Console.WriteLine(HostConfig.Instance); + } + + private void OnShutdown() + { + _logger.Info("Application is shutting down"); + try + { + _appModuleStarter.Stop(); + QueueManager.Instance.Dispose(); + } + catch (Exception ex) + { + _logger.Error("Failed to shutdown", ex); + } + + _logger.Info("Application shutdown completed."); + } + + private void OnShutdownRequested(object sender, ShuttingDownEventArgs shuttingDownEventArgs) + { + //too early in the startup process. + if (_applicationLifetime == null) + return; + + shuttingDownEventArgs.CanShutdown = PendingRequestTrackingMiddleware.NumberOfRequests == 0; + if (!shuttingDownEventArgs.CanShutdown) return; + + _logger.Info("Queue system requested shutdown."); + _applicationLifetime.StopApplication(); + } + + private IDbConnection OpenConnection(ClaimsPrincipal arg) + { + var connection = new SqlConnection(_configuration.GetConnectionString("Db")); + try + { + connection.Open(); + } + catch (SqlException) + { + Thread.Sleep(500); + connection.Open(); + } + + return connection; + } + + private void RegisterConfigurationStores(IServiceCollection services) + { + services.AddTransient(typeof(IConfiguration<>), typeof(ConfigWrapper<>)); + + if (!ServerConfig.Instance.IsLive) + services.AddTransient(x => + new DatabaseStore(() => OpenConnection(CoderrClaims.SystemPrincipal))); + } + + private void UpgradeDatabaseSchema() + { + // Don't run for new installations + if (!HostConfig.Instance.IsConfigured) + return; + + try + { + var tools = new SqlServerTools(() => OpenConnection(CoderrClaims.SystemPrincipal)); + tools.CreateTables(); + } + catch (Exception ex) + { + _logger.Fatal("DB Migration failed.", ex); + Err.Report(ex, new { Migration = true }); + } + } + + public void Start() + { + if (HostConfig.Instance.IsConfigured) + { + HostConfig.Instance.TriggeredConfigured(); + } + } + + public void AddAuthentication(AuthenticationBuilder authenticationBuilder) + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ExecuteDirectlyMessageBus.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ExecuteDirectlyMessageBus.cs new file mode 100644 index 00000000..bf196666 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ExecuteDirectlyMessageBus.cs @@ -0,0 +1,89 @@ +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Coderr.Server.Abstractions.Boot; +using DotNetCqs; +using DotNetCqs.MessageProcessor; + +namespace Coderr.Server.WebSite.Infrastructure.Cqs.Adapters +{ + [ContainerService(RegisterAsSelf = true)] + public class ExecuteDirectlyMessageBus : IMessageBus + { + private readonly IMessageBus _outboundBus; + private readonly IMessageInvoker _messageInvoker; + private static readonly SemaphoreSlim _lock = new SemaphoreSlim(1); + + public ExecuteDirectlyMessageBus(IMessageInvoker messageInvoker, IMessageBus outboundBus) + { + _messageInvoker = messageInvoker; + _outboundBus = outboundBus; + } + + public async Task SendAsync(ClaimsPrincipal principal, object message) + { + await _lock.WaitAsync(); + try + { + var outboundMessages = new List(); + var ctx = new InvocationContext("Directly", principal, _messageInvoker, outboundMessages); + await _messageInvoker.ProcessAsync(ctx, message); + foreach (var outboundMessage in outboundMessages) + await _outboundBus.SendAsync(principal, outboundMessage); + } + finally + { + _lock.Release(); + } + } + + public async Task SendAsync(ClaimsPrincipal principal, Message message) + { + await _lock.WaitAsync(); + try + { + var outboundMessages = new List(); + var ctx = new InvocationContext("Directly", principal, _messageInvoker, outboundMessages); + await _messageInvoker.ProcessAsync(ctx, message); + foreach (var outboundMessage in outboundMessages) await _outboundBus.SendAsync(principal, outboundMessage); + } + finally + { + _lock.Release(); + } + } + + public async Task SendAsync(Message message) + { + await _lock.WaitAsync(); + try + { + var outboundMessages = new List(); + var ctx = new InvocationContext("Directly", null, _messageInvoker, outboundMessages); + await _messageInvoker.ProcessAsync(ctx, message); + foreach (var outboundMessage in outboundMessages) await _outboundBus.SendAsync(outboundMessage); + } + finally + { + _lock.Release(); + } + } + + public async Task SendAsync(object message) + { + await _lock.WaitAsync(); + try + { + var outboundMessages = new List(); + var ctx = new InvocationContext("Directly", null, _messageInvoker, outboundMessages); + await _messageInvoker.ProcessAsync(ctx, message); + foreach (var outboundMessage in outboundMessages) await _outboundBus.SendAsync(outboundMessage); + } + finally + { + _lock.Release(); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/IncludeNonPublicAndUseCamelCaseContractResolver.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/IncludeNonPublicAndUseCamelCaseContractResolver.cs new file mode 100644 index 00000000..6d0fe520 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/IncludeNonPublicAndUseCamelCaseContractResolver.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Coderr.Server.WebSite.Infrastructure.Cqs.Adapters +{ + /// + /// Used by JSON.NET to be able to deserialize properties with private setters. + /// + public class IncludeNonPublicAndUseCamelCaseContractResolver : CamelCasePropertyNamesContractResolver + { + //protected override List GetSerializableMembers(Type objectType) + //{ + // var members = base.GetSerializableMembers(objectType); + // return members.Where(m => !m.Name.EndsWith("k__BackingField")).ToList(); + //} + + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + //TODO: Maybe cache + var prop = base.CreateProperty(member, memberSerialization); + + if (!prop.Writable) + { + var property = member as PropertyInfo; + if (property != null) + { + var hasPrivateSetter = property.GetSetMethod(true) != null; + prop.Writable = hasPrivateSetter; + } + } + + return prop; + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/RoutingMessageBus.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/RoutingMessageBus.cs new file mode 100644 index 00000000..ca3324e0 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/RoutingMessageBus.cs @@ -0,0 +1,37 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using DotNetCqs; +using DotNetCqs.Queues; + +namespace Coderr.Server.WebSite.Infrastructure.Cqs.Adapters +{ + public class RoutingMessageBus : IMessageBus + { + private readonly IMessageRouter _router; + + public RoutingMessageBus(IMessageRouter router) + { + _router = router; + } + + public async Task SendAsync(ClaimsPrincipal principal, object message) + { + await SendAsync(principal, new Message(message)); + } + + public async Task SendAsync(ClaimsPrincipal principal, Message message) + { + await _router.SendAsync(principal, message); + } + + public async Task SendAsync(Message message) + { + await _router.SendAsync(message); + } + + public async Task SendAsync(object message) + { + await SendAsync(new Message(message)); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ScopeCommitter.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ScopeCommitter.cs new file mode 100644 index 00000000..474b9173 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ScopeCommitter.cs @@ -0,0 +1,25 @@ +using System; +using Griffin.Data; + +namespace Coderr.Server.WebSite.Infrastructure.Cqs.Adapters +{ + /// + /// Commits unit of work when scope is closing if no exceptions have been thrown in the current scope + /// + public class ScopeCommitter : IDisposable + { + private readonly IAdoNetUnitOfWork _uow; + + public ScopeCommitter(IAdoNetUnitOfWork uow) + { + _uow = uow; + } + + public Exception Exception { get; set; } + + public void Dispose() + { + if (Exception == null) _uow.SaveChanges(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ScopeFactory.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ScopeFactory.cs new file mode 100644 index 00000000..873ca4c7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/Adapters/ScopeFactory.cs @@ -0,0 +1,30 @@ +using System; +using DotNetCqs.DependencyInjection; +using DotNetCqs.DependencyInjection.Microsoft; + +namespace Coderr.Server.WebSite.Infrastructure.Cqs.Adapters +{ + internal class ScopeFactory : IHandlerScopeFactory + { + private readonly Func _serviceProviderAccessor; + private MicrosoftHandlerScopeFactory _scopeFactory; + + public ScopeFactory(Func serviceProviderAccessor) + { + _serviceProviderAccessor = serviceProviderAccessor; + } + + public IHandlerScope CreateScope() + { + if (_scopeFactory == null) + { + var provider = _serviceProviderAccessor(); + if (provider == null) + throw new InvalidOperationException("container have not been setup properly yet."); + _scopeFactory = new MicrosoftHandlerScopeFactory(provider); + } + + return _scopeFactory.CreateScope(); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/CqsObjectMapper.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/CqsObjectMapper.cs new file mode 100644 index 00000000..f5e93bca --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/CqsObjectMapper.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Coderr.Server.Api; +using Coderr.Server.Infrastructure.Messaging; +using Coderr.Server.WebSite.Infrastructure.Cqs.Adapters; +using Newtonsoft.Json; + +namespace Coderr.Server.WebSite.Infrastructure.Cqs +{ + /// + /// Used to map objects that is received from other languages (i.e. using different techniques to identify the .NET + /// type). + /// + /// + /// + /// You typically include the .NET type (as a string) or the CQS object name (as long as you've mapped the .NET + /// types using Map() or ScanAssembly()). + /// + /// + public class CqsObjectMapper + { + private readonly Dictionary _cqsTypes = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings + { + ContractResolver = new IncludeNonPublicAndUseCamelCaseContractResolver(), + NullValueHandling = NullValueHandling.Ignore, + ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, + + // Typescript requires numbers for easier enum handling + // otherwise we have to redefine all enums so that the keys are strings. + + //Converters = new List { new StringEnumConverter() } + }; + + public bool IsEmpty => _cqsTypes.Count == 0; + + /// + /// Deserialize incoming object + /// + /// Name of the dot net type or CQS. + /// Received JSON. + /// CQS object + public object Deserialize(string dotNetTypeOrCqsName, string json) + { + var type = Type.GetType(dotNetTypeOrCqsName); + if (type == null && !_cqsTypes.TryGetValue(dotNetTypeOrCqsName, out type)) + return null; + + return string.IsNullOrEmpty(json) + ? Activator.CreateInstance(type, true) + : JsonConvert.DeserializeObject(json, type, _jsonSerializerSettings); + } + + /// + /// We just have this method to make sure that the serialization is exactly the same in both directions. + /// + /// + /// + public string Serialize(object value) + { + return JsonConvert.SerializeObject(value, _jsonSerializerSettings); + } + + /// + /// Determines whether the type implements the command handler interface + /// + /// The type. + /// true if the objects is a command handler; otherwise false + private static bool IsCqsType(Type cqsType) + { + if (cqsType.IsAbstract || cqsType.IsInterface) + return false; + + + if (cqsType.GetCustomAttribute(true) != null) + return true; + + var attrs = cqsType.GetCustomAttributes() + .Select(x => x.GetType()) + .Any(x => x.Name == "MessageAttribute" || x.Name == "CommandAttribute" || x.Name == "QueryAttribute"); + return attrs; + } + + /// + /// Map a type directly. + /// + /// Must implement one of the handler interfaces. + /// type + /// + /// ' + type.FullName + ' do not implement one of the handler interfaces. + /// or + /// ' + type.FullName + ' is abstract or an interface. + /// + /// + /// Duplicate mappings for a name (two different handlers may not have + /// the same class name). + /// + /// + /// Required if the HTTP client do not supply the full .NET type name (just the class name of the command/query). + /// + public void Map(Type type) + { + if (type == null) throw new ArgumentNullException("type"); + + if (!IsCqsType(type)) + throw new ArgumentException($"'{type.FullName}' is not a CQS object."); + + if (_cqsTypes.ContainsKey(type.Name)) + throw new InvalidOperationException( + $"Duplicate mappings for name '{type.Name}'. '{type.FullName}' and '{_cqsTypes[type.Name].FullName}'."); + + _cqsTypes.Add(type.Name, type); + } + + /// + /// Scan assembly for handlers. + /// + /// + /// Required if the HTTP client do not supply the full .NET type name (just the class name of the command/query). + /// + /// The assembly to scan for handlers. + /// + /// Duplicate mappings for a name (two different handlers may not have + /// the same class name). + /// + public void ScanAssembly(Assembly assembly) + { + foreach (var type in assembly.GetTypes()) + { + if (!IsCqsType(type)) + continue; + + if (_cqsTypes.ContainsKey(type.Name)) + throw new InvalidOperationException( + $"Duplicate mappings for '{type.Name}': '{type.FullName}' and '{_cqsTypes[type.Name].FullName}'."); + + _cqsTypes.Add(type.Name, type); + } + } + + public bool HasType(string cqsName) + { + return _cqsTypes.ContainsKey(cqsName); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/RegisterCqsServices.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/RegisterCqsServices.cs new file mode 100644 index 00000000..445d072c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Cqs/RegisterCqsServices.cs @@ -0,0 +1,256 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Coderr.Client; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.Abstractions.Security; +using Coderr.Server.App.Core.Accounts; +using Coderr.Server.Domain.Core.Incidents.Events; +using Coderr.Server.Infrastructure.Messaging; +using Coderr.Server.SqlServer; +using Coderr.Server.WebSite.Infrastructure.Adapters; +using Coderr.Server.WebSite.Infrastructure.Cqs.Adapters; +using DotNetCqs; +using DotNetCqs.Bus; +using DotNetCqs.DependencyInjection; +using DotNetCqs.MessageProcessor; +using DotNetCqs.Queues; +using Griffin.Data; +using log4net; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; + +namespace Coderr.Server.WebSite.Infrastructure.Cqs +{ + public class RegisterCqsServices : IAppModule + { + private readonly ILog _log = LogManager.GetLogger(typeof(RegisterCqsServices)); + private readonly CancellationTokenSource _cancelTokenSource = new CancellationTokenSource(); + private QueueListener _queueListener; + private Task _queueListenerTask; + + public void Configure(ConfigurationContext context) + { + var assembly = typeof(IAccountService).Assembly; + context.Services.RegisterMessageHandlers(assembly); + + assembly = typeof(SqlServerTools).Assembly; + context.Services.RegisterMessageHandlers(assembly); + + context.Services.AddScoped(); + context.Services.AddScoped(); + context.Services.AddSingleton(QueueManager.Instance.QueueProvider); + context.Services.AddSingleton(MessageRouter.Instance); + context.Services.AddSingleton(MessageRouter.Instance); + + context.Services.AddSingleton(x => + { + var appQueueName = + ServerConfig.Instance.IsLive + ? context.Configuration.GetSection("MessageQueue")["AppQueue"] + : "Messaging"; + var appQueue = x.GetService().Open(appQueueName); + + var reportQueueName = + ServerConfig.Instance.IsLive + ? context.Configuration.GetSection("MessageQueue")["ReportQueue"] + : "ErrorReports"; + var reportQueue = x.GetService().Open(reportQueueName); + + MessageRouter.Instance.RegisterAppQueue(appQueue); + MessageRouter.Instance.RegisterReportQueue(reportQueue); + return new RoutingMessageBus(MessageRouter.Instance); + }); + + context.Services.AddScoped(); + context.Services.AddScoped(CreateMessageInvoker); + + _log.Debug("Creating listeners.."); + _queueListener = CreateQueueListener(context); + } + + private QueueListener CreateQueueListener(ConfigurationContext context) + { + if (ServerConfig.Instance.IsLive) + { + var appQueueName = context.Configuration.GetSection("MessageQueue")["AppQueue"]; + var reportQueueName = context.Configuration.GetSection("MessageQueue")["ReportQueue"]; + return ConfigureQueueListener(context, appQueueName, appQueueName, reportQueueName); + } + + return ConfigureQueueListener(context, "Messaging", "Messaging", "ErrorReports"); + } + + private QueueListener ConfigureQueueListener(ConfigurationContext context, string inboundQueueName, string outboundQueueName, string reportAnalyzerQueue) + { + var inboundQueue = QueueManager.Instance.GetQueue(inboundQueueName); + var outboundQueue = inboundQueueName == outboundQueueName + ? inboundQueue + : QueueManager.Instance.GetQueue(outboundQueueName); + var scopeFactory = new ScopeFactory(context.ServiceProvider); + + MessageRouter.Instance.RegisterAppQueue(outboundQueue); + MessageRouter.Instance.RegisterReportQueue(QueueManager.Instance.GetQueue(reportAnalyzerQueue)); + + _log.Debug($"Loading listener {inboundQueueName} with outbound {outboundQueueName} and report queue {reportAnalyzerQueue}."); + var listener = new QueueListener(inboundQueue, MessageRouter.Instance, scopeFactory) + { + RetryAttempts = new[] + {TimeSpan.FromMilliseconds(500), TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(2)}, + MessageInvokerFactory = scope => new MessageInvoker(scope), + }; + listener.PoisonMessageDetected += (sender, args) => + { + //Err.Report(args.Exception, new { args.Message }); + _log.Error($"{inboundQueueName} Poison message: {args.Message.Body}", args.Exception); + }; + listener.ScopeCreated += (sender, args) => + { + _log.Debug( + $"CoreApp {inboundQueueName} scope created: {args.Scope.GetHashCode()} for {args.Message.Body.ToString().Replace("\r\n", " ")}."); + var accessor = args.Scope.ResolveDependency().First(); + accessor.Principal = args.Principal; + _log.Info( + $"CoreApp {inboundQueueName} scope: {args.Scope.GetHashCode()} mapped to {accessor.GetHashCode()}, Credentials: {args.Principal.ToFriendlyString()}."); + }; + listener.ScopeClosing += (sender, args) => + { + if (args.Exception != null) + return; + + _log.Debug($"CoreApp {inboundQueueName} {args.Scope.GetHashCode()} Saving changes now for {args.Message.Body}"); + var all = args.Scope.ResolveDependency().ToList(); + all[0].SaveChanges(); + }; + listener.MessageInvokerFactory = MessageInvokerFactory; + return listener; + } + + public void Start(StartContext context) + { + _queueListenerTask = _queueListener.RunAsync(_cancelTokenSource.Token); + _queueListenerTask.ContinueWith(OnListenerStopped); + } + + private void OnListenerStopped(Task obj) + { + + + } + + public void Stop() + { + try + { + _log.Debug("Shutting down CQS listeners."); + _cancelTokenSource.Cancel(); + _queueListenerTask.Wait(5000); + } + catch (TaskCanceledException) + { + _log.Debug("Task has been canceled1."); + } + catch (Exception ex) + { + if (ex is AggregateException ae && ae.InnerException is TaskCanceledException) + { + _log.Debug("Task has been canceled."); + } + else + { + _log.Error("Failed to wait 10s.", ex); + } + } + + _log.Debug("Shutting down CQS listeners was successful."); + } + + private IMessageInvoker CreateMessageInvoker(IServiceProvider x) + { + var invoker = new MessageInvoker(new HandlerScopeWrapper(x)); + invoker.HandlerMissing += (sender, args) => + { + if (args.Message.Body is IncidentCreated) + return; + + _log.Error("CoreCqs No handler for " + args.Message.Body.GetType()); + try + { + throw new NoHandlerRegisteredException( + "Failed to find a handler for " + args.Message.Body.GetType()); + } + catch (Exception ex) + { + Err.Report(ex, new { args.Message.Body }); + } + }; + + invoker.InvokingHandler += (sender, args) => + { + _log.Debug( + $"CoreCqs scope {args.Scope.GetHashCode()} Invoking {JsonConvert.SerializeObject(args.Message)} ({args.Handler.GetType()})."); + }; + invoker.HandlerInvoked += (sender, args) => + { + if (args.Exception == null) + return; + + var closer = args.Scope.ResolveDependency().First(); + closer.Exception = args.Exception; + + _log.Error( + $"CoreCqs Ran {args.Handler}, took {args.ExecutionTime.TotalMilliseconds}ms, but FAILED, principal: " + args.Principal.ToFriendlyString(), + args.Exception); + + Err.Report(args.Exception, new + { + args.Message.Body, + HandlerType = args.Handler.GetType(), + args.ExecutionTime + }); + }; + return invoker; + } + + private IMessageInvoker MessageInvokerFactory(IHandlerScope arg) + { + var invoker = new MessageInvoker(arg); + invoker.HandlerMissing += (sender, args) => + { + _log.Warn("Handler missing for " + args.Message.Body.GetType()); + }; + invoker.HandlerInvoked += (sender, args) => + { + _log.Debug(args.Message.Body); + if (args.Exception == null) + return; + + Err.Report(args.Exception, new + { + args.Message.Body, + HandlerType = args.Handler.GetType(), + args.ExecutionTime + }); + _log.Error( + $"Ran {args.Handler} with {args.Message.Body}, took {args.ExecutionTime.TotalMilliseconds}ms, but FAILED, principal: " + args.Principal.ToFriendlyString(), + args.Exception); + }; + return invoker; + } + + public static bool IsQuery(object cqsObject) + { + if (cqsObject == null) throw new ArgumentNullException(nameof(cqsObject)); + var baseType = cqsObject.GetType().BaseType; + while (baseType != null) + { + if (baseType.FullName.StartsWith("DotNetCqs.Query")) + return true; + baseType = baseType.BaseType; + } + return false; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Filters/PrincipalActionFilter.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Filters/PrincipalActionFilter.cs new file mode 100644 index 00000000..26075398 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Filters/PrincipalActionFilter.cs @@ -0,0 +1,21 @@ +using System.Security.Claims; +using Coderr.Server.WebSite.Infrastructure.Boot; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Coderr.Server.WebSite.Infrastructure.Filters +{ + public class PrincipalActionFilter : ActionFilterAttribute + { + public override void OnActionExecuting(ActionExecutingContext filterContext) + { + //DependencyResolver.Current. + if (filterContext.HttpContext.User is ClaimsPrincipal principal) + { + var setter = (PrincipalWrapper)filterContext.HttpContext.RequestServices.GetService(typeof(PrincipalWrapper)); + setter.Principal = principal; + } + + base.OnActionExecuting(filterContext); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Filters/TransactionalAttribute.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Filters/TransactionalAttribute.cs new file mode 100644 index 00000000..8f2182fd --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Filters/TransactionalAttribute.cs @@ -0,0 +1,36 @@ +using Coderr.Server.Abstractions; +using Griffin.Data; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace Coderr.Server.WebSite.Infrastructure.Filters +{ + public class TransactionalAttribute : ActionFilterAttribute + { + public override void OnActionExecuted(ActionExecutedContext filterContext) + { + if (filterContext.HttpContext.Items["IgnoreTransaction"] as bool? == true) + return; + + if (!HostConfig.Instance.IsConfigured) + { + return; + } + + var isMethodTransactional = true;/*filterContext.HttpContext.Request.Method == "POST" || + filterContext.ActionDescriptor.FilterDescriptors.Any(x => + x.Filter.GetType() == typeof(TransactionalAttribute));*/ + if (filterContext.Exception == null && filterContext.ModelState.IsValid && isMethodTransactional) + { + var uow = (IAdoNetUnitOfWork) filterContext.HttpContext.RequestServices.GetService(typeof(IAdoNetUnitOfWork)); + + //NULL when the setup is running. + if (uow == null) + return; + + uow.SaveChanges(); + } + + base.OnActionExecuted(filterContext); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/JwtHelper.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/JwtHelper.cs new file mode 100644 index 00000000..8569deba --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/JwtHelper.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IdentityModel.Tokens.Jwt; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; + +namespace Coderr.Server.WebSite.Infrastructure +{ + public class JwtHelper + { + public static void Configure(JwtBearerOptions x) + { + x.IncludeErrorDetails = true; + x.RequireHttpsMetadata = false; + x.Audience = JwtSettings.Audience; + x.ClaimsIssuer = JwtSettings.Issuer; + x.TokenValidationParameters = new TokenValidationParameters + { + IssuerSigningKey = JwtSettings.TokenKey, + ValidAudience = JwtSettings.Audience, + ValidIssuer = JwtSettings.Issuer + }; + } + + public static string GenerateToken(ClaimsIdentity identity) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = identity, + Expires = DateTime.UtcNow.AddDays(7), + Issuer = JwtSettings.Issuer, + Audience = JwtSettings.Audience, + + SigningCredentials = new SigningCredentials(JwtSettings.TokenKey, SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/JwtSettings.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/JwtSettings.cs new file mode 100644 index 00000000..e82926b1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/JwtSettings.cs @@ -0,0 +1,23 @@ +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace Coderr.Server.WebSite.Infrastructure +{ + public class JwtSettings + { + static JwtSettings() + { + var mySecret = "fs9fsd@£20@lffjLDdjsaSLS%&¤#"; + TokenKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(mySecret)); + + Issuer = "https://coderr.io"; + Audience = "https://coderr.io"; + } + + public static string Audience { get; } + + public static string Issuer { get; } + + public static SymmetricSecurityKey TokenKey { get; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/KeyValuePairConverter.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/KeyValuePairConverter.cs new file mode 100644 index 00000000..4dc2c3ba --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/KeyValuePairConverter.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Coderr.Server.WebSite.Infrastructure +{ + public class KeyValuePairConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, object value, + JsonSerializer serializer) + { + var list = (List>)value; + writer.WriteStartArray(); + foreach (var item in list) + { + writer.WriteStartObject(); + writer.WritePropertyName(item.Key); + writer.WriteValue(item.Value); + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(List>); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/ApplicationServiceManagerSettingsWithDefaultOn.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/ApplicationServiceManagerSettingsWithDefaultOn.cs new file mode 100644 index 00000000..d2ff3ccb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/ApplicationServiceManagerSettingsWithDefaultOn.cs @@ -0,0 +1,27 @@ +using System; +using Coderr.Server.Abstractions.Boot; +using Griffin.ApplicationServices; + +namespace Coderr.Server.WebSite.Infrastructure.Modules +{ + /// + /// The default AppConfigServiceSettings will report off if the key is missing. We want the opposite. + /// + internal class ApplicationServiceManagerSettingsWithDefaultOn : ISettingsRepository + { + private readonly IConfigurationSection _configSection; + + public ApplicationServiceManagerSettingsWithDefaultOn(IConfigurationSection configSection) + { + _configSection = configSection; + } + + /// + public bool IsEnabled(Type type) + { + if (type == null) throw new ArgumentNullException("type"); + var value = _configSection[type.Name + ".Enabled"] ?? "true"; + return value == "true"; + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/ApplicationServices.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/ApplicationServices.cs new file mode 100644 index 00000000..1e395742 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/ApplicationServices.cs @@ -0,0 +1,37 @@ +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.WebSite.Infrastructure.Adapters; +using Griffin.ApplicationServices; + +namespace Coderr.Server.WebSite.Infrastructure.Modules +{ + public class ApplicationServices : IAppModule + { + private ApplicationServiceManager _appManager; + private IConfigurationSection _configuration; + + public void Start(StartContext context) + { + var adapter = new DependencyInjectionAdapter(context.ServiceProvider); + _appManager = _appManager = new ApplicationServiceManager(adapter) + { + Settings = new ApplicationServiceManagerSettingsWithDefaultOn(_configuration) + }; + _appManager.ServiceFailed += OnServiceFailed; + _appManager.Start(); + } + + public void Stop() + { + _appManager?.Stop(); + } + + public void Configure(ConfigurationContext context) + { + _configuration = context.Configuration.GetSection("ApplicationServices"); + } + + private void OnServiceFailed(object sender, ApplicationServiceFailedEventArgs e) + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/BackgroundJobsForAnalyzer.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/BackgroundJobsForAnalyzer.cs new file mode 100644 index 00000000..c1b1d3b3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/BackgroundJobsForAnalyzer.cs @@ -0,0 +1,61 @@ +using System; +using System.Diagnostics; +using Coderr.Client; +using Coderr.Server.Abstractions; +using Coderr.Server.ReportAnalyzer.Abstractions.Boot; +using Coderr.Server.WebSite.Infrastructure.Adapters; +using Griffin.ApplicationServices; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.WebSite.Infrastructure.Modules +{ + public class BackgroundJobsForAnalyzer : IReportAnalyzerModule + { + private BackgroundJobManager _backgroundJobManager; + private IConfiguration _configuration; + private ILog _logger = LogManager.GetLogger(typeof(BackgroundJobManager)); + + public void Start(StartContext context) + { + var adapter = new DependencyInjectionAdapter(context.ServiceProvider); + + _backgroundJobManager = new BackgroundJobManager(adapter) + { + ExecuteSequentially = true, + StartInterval = TimeSpan.FromSeconds(Debugger.IsAttached ? 0 : 10), + ExecuteInterval = TimeSpan.FromMinutes(3) + }; + _backgroundJobManager.JobFailed += OnBackgroundJobFailed; + _backgroundJobManager.StartInterval = TimeSpan.FromSeconds(Debugger.IsAttached ? 0 : 10); + _backgroundJobManager.ExecuteInterval = TimeSpan.FromSeconds(Debugger.IsAttached ? 10 : 30); + _backgroundJobManager.ScopeClosing += OnBackgroundJobScopeClosing; + _backgroundJobManager.Start(); + } + + public void Stop() + { + _backgroundJobManager?.Stop(); + } + + public void Configure(ConfigurationContext context) + { + _configuration = context.Configuration; + } + + private void OnBackgroundJobFailed(object sender, BackgroundJobFailedEventArgs e) + { + _logger.Error("Report Job failed: " + e.Job, e.Exception); + Err.Report(e.Exception, new + { + JobType = e.Job.GetType().FullName + }); + } + + private void OnBackgroundJobScopeClosing(object sender, ScopeClosingEventArgs e) + { + if (e.Successful) + e.Scope.Resolve().SaveChanges(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/BackgroundJobsForApp.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/BackgroundJobsForApp.cs new file mode 100644 index 00000000..73697cfe --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/BackgroundJobsForApp.cs @@ -0,0 +1,62 @@ +using System; +using System.Diagnostics; +using Coderr.Client; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.WebSite.Infrastructure.Adapters; +using Griffin.ApplicationServices; +using Griffin.Data; +using log4net; + +namespace Coderr.Server.WebSite.Infrastructure.Modules +{ + public class BackgroundJobsForApp : IAppModule + { + private BackgroundJobManager _backgroundJobManager; + private IConfiguration _configuration; + private ILog _logger = LogManager.GetLogger(typeof(BackgroundJobManager)); + + public void Start(StartContext context) + { + var adapter = new DependencyInjectionAdapter(context.ServiceProvider); + + _backgroundJobManager = new BackgroundJobManager(adapter) + { + ExecuteSequentially = true, + StartInterval = TimeSpan.FromSeconds(Debugger.IsAttached ? 0 : 10), + ExecuteInterval = TimeSpan.FromMinutes(3) + }; + _backgroundJobManager.JobFailed += OnBackgroundJobFailed; + _backgroundJobManager.StartInterval = TimeSpan.FromSeconds(Debugger.IsAttached ? 0 : 10); + _backgroundJobManager.ExecuteInterval = TimeSpan.FromSeconds(Debugger.IsAttached ? 10 : 30); + _backgroundJobManager.ScopeClosing += OnBackgroundJobScopeClosing; + _backgroundJobManager.Start(); + } + + + public void Stop() + { + _backgroundJobManager?.Stop(); + } + + public void Configure(ConfigurationContext context) + { + _configuration = context.Configuration; + } + + private void OnBackgroundJobFailed(object sender, BackgroundJobFailedEventArgs e) + { + _logger.Error("Job failed: " + e.Job, e.Exception); + Err.Report(e.Exception, new + { + JobType = e.Job.GetType().FullName + }); + } + + private void OnBackgroundJobScopeClosing(object sender, ScopeClosingEventArgs e) + { + if (e.Successful) + e.Scope.Resolve().SaveChanges(); + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/DbConnectionConfig.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/DbConnectionConfig.cs new file mode 100644 index 00000000..770393c9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/DbConnectionConfig.cs @@ -0,0 +1,43 @@ +using System; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.SqlServer; +using Griffin.Data; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.WebSite.Infrastructure.Modules +{ + public class DbConnectionConfig : IAppModule + { + public void Configure(ConfigurationContext context) + { + context.Services.AddScoped(x => OpenConnection()); + context.Services.AddScoped(x => x.GetRequiredService().BeginTransaction()); + context.Services.AddScoped(x => new UnitOfWorkWithTransaction((DbTransaction)x.GetRequiredService())); + } + + internal static IDbConnection OpenConnection() + { + var db = HostConfig.Instance.ConnectionString; + if (db == null) + { + throw new InvalidOperationException("Missing the connection string 'Db'."); + } + var con = new SqlConnection(db); + + con.Open(); + return con; + } + + public void Start(StartContext context) + { + } + + public void Stop() + { + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/RegisterContainerServices.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/RegisterContainerServices.cs new file mode 100644 index 00000000..0aababb8 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/Modules/RegisterContainerServices.cs @@ -0,0 +1,26 @@ +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.App.Core.Accounts; +using Coderr.Server.SqlServer; + +namespace Coderr.Server.WebSite.Infrastructure.Modules +{ + public class RegisterContainerServices : IAppModule + { + public void Configure(ConfigurationContext context) + { + var assembly = typeof(IAccountService).Assembly; + context.Services.RegisterContainerServices(assembly); + + assembly = typeof(SqlServerTools).Assembly; + context.Services.RegisterContainerServices(assembly); + } + + public void Start(StartContext context) + { + } + + public void Stop() + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/PendingRequestTrackingMiddleware.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/PendingRequestTrackingMiddleware.cs new file mode 100644 index 00000000..2dacb803 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/PendingRequestTrackingMiddleware.cs @@ -0,0 +1,32 @@ +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Coderr.Server.WebSite.Infrastructure +{ + public class PendingRequestTrackingMiddleware + { + private readonly RequestDelegate _next; + private static long _numberOfRequests; + + public PendingRequestTrackingMiddleware(RequestDelegate next) + { + _next = next; + } + + public static long NumberOfRequests => _numberOfRequests; + + public async Task Invoke(HttpContext httpContext) + { + Interlocked.Increment(ref _numberOfRequests); + try + { + await _next(httpContext); + } + finally + { + Interlocked.Decrement(ref _numberOfRequests); + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/PushClient.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/PushClient.cs new file mode 100644 index 00000000..29a8383c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/PushClient.cs @@ -0,0 +1,90 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Coderr.Server.Abstractions; +using Coderr.Server.Abstractions.Config; +using Coderr.Server.Domain.Modules.UserNotifications; +using Coderr.Server.Infrastructure.Configuration; +using Coderr.Server.ReportAnalyzer.UserNotifications; +using Coderr.Server.ReportAnalyzer.UserNotifications.Dtos; +using Coderr.Server.WebPush; +using Coderr.Server.WebPush.Model; +using log4net; +using Newtonsoft.Json; +using NotificationAction = Coderr.Server.WebPush.Model.NotificationAction; + +namespace Coderr.Server.WebSite.Infrastructure.WebPush +{ + public class PushClient : IWebPushClient + { + private readonly VapidDetails _vapid; + private ILog _logger = LogManager.GetLogger(typeof(PushClient)); + private DateTime _dateLogged = DateTime.Today; + + public PushClient(IConfiguration pushConfiguration, IConfiguration baseConfiguration) + { + if (ServerConfig.Instance.IsLive) + { + baseConfiguration.Value.SupportEmail = "support@coderr.io"; + } + + if (pushConfiguration.Value.PublicKey == null) + { + if (_dateLogged < DateTime.Today) + { + _logger.Debug("got no configuration"); + _dateLogged = DateTime.Today; + } + + return; + } + + _vapid = new VapidDetails("mailto:" + baseConfiguration.Value.SupportEmail, pushConfiguration.Value.PublicKey, pushConfiguration.Value.PrivateKey); + } + + public async Task SendNotification(BrowserSubscription subscription, Notification notification) + { + if (_vapid?.PrivateKey == null) + { + _logger.Error("WebPush config is missing keys for accountId " + subscription.AccountId); + return; + } + + var dto = new PushSubscription(subscription.Endpoint, subscription.PublicKey, + subscription.AuthenticationSecret); + + var dtoNotification = new PushNotification(notification.Body) + { + Actions = notification.Actions.Select(x => new NotificationAction(x.Action, x.Title)).ToList(), + Body = notification.Body, + Title = notification.Title, + Badge = notification.Badge, + Data = notification.Data, + Icon = notification.Icon, + Image = notification.Image, + Lang = notification.Lang, + RequireInteraction = notification.RequireInteraction, + Tag = notification.Tag, + Timestamp = notification.Timestamp + }; + + try + { + _logger.Debug( + $"Sending Push notification using {JsonConvert.SerializeObject(_vapid)} to {JsonConvert.SerializeObject(subscription)}"); + var client = new WebPushClient(_vapid); + await client.NotifyAsync(dto, dtoNotification); + } + catch (WebPushException ex) + { + if (ex.Message == "Subscription no longer valid") + { + throw new InvalidSubscriptionException( + $"Subscription for {subscription.Endpoint} is no longer valid.", ex); + } + + throw; + } + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/WebPushAppModule.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/WebPushAppModule.cs new file mode 100644 index 00000000..9cfde9d1 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/WebPushAppModule.cs @@ -0,0 +1,22 @@ +using Coderr.Server.Abstractions.Boot; +using Coderr.Server.ReportAnalyzer.UserNotifications; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.WebSite.Infrastructure.WebPush +{ + public class WebPushAppModule : IAppModule + { + public void Configure(ConfigurationContext context) + { + context.Services.AddScoped(typeof(IWebPushClient), typeof(PushClient)); + } + + public void Start(StartContext context) + { + } + + public void Stop() + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/WebPushReportModule.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/WebPushReportModule.cs new file mode 100644 index 00000000..70fed1e7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/WebPush/WebPushReportModule.cs @@ -0,0 +1,22 @@ +using Coderr.Server.ReportAnalyzer.Abstractions.Boot; +using Coderr.Server.ReportAnalyzer.UserNotifications; +using Microsoft.Extensions.DependencyInjection; + +namespace Coderr.Server.WebSite.Infrastructure.WebPush +{ + public class WebPushReportModule : IReportAnalyzerModule + { + public void Configure(ConfigurationContext context) + { + context.Services.AddScoped(typeof(IWebPushClient), typeof(PushClient)); + } + + public void Start(StartContext context) + { + } + + public void Stop() + { + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Infrastructure/WebSockets/IWebSocketHub.cs b/src/Server/Coderr.Server.WebSite/Infrastructure/WebSockets/IWebSocketHub.cs new file mode 100644 index 00000000..911a565c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Infrastructure/WebSockets/IWebSocketHub.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; + +namespace Coderr.Server.WebSite.Infrastructure.WebSockets +{ + interface IWebSocketHub + { + Task Process(CancellationToken contextRequestAborted, WebSocket webSocket, ClaimsPrincipal claimsPrincipal); + } + + public class WebSocketConnection + { + private WebSocket _socket; + private ClaimsPrincipal _claimsPrincipal; + + public WebSocketConnection(WebSocket socket, ClaimsPrincipal claimsPrincipal) + { + _socket = socket; + _claimsPrincipal = claimsPrincipal; + } + } + + public class WebSocketHub : IWebSocketHub + { + private List _connections = new List(); + + public async Task Process(CancellationToken contextRequestAborted, WebSocket webSocket, + ClaimsPrincipal claimsPrincipal) + { + var connection = new WebSocketConnection(webSocket, claimsPrincipal); + lock (_connections) + { + _connections.Add(connection); + } + + while (!contextRequestAborted.IsCancellationRequested) + { + await Task.Delay(1000, contextRequestAborted); + } + + lock (_connections) + { + _connections.Remove(connection); + } + } + + public void Send() + { + + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/ModelStateExtensions.cs b/src/Server/Coderr.Server.WebSite/ModelStateExtensions.cs new file mode 100644 index 00000000..17f16dfb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/ModelStateExtensions.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Coderr.Server.WebSite +{ + public static class ModelStateExtensions + { + public static string ToSummary(this ModelStateDictionary modelState) + { + var sb = new StringBuilder(); + foreach (var kvp in modelState) + { + sb.Append($"{kvp.Key}: "); + var errors = kvp.Value.Errors.Select(x => x.ErrorMessage); + sb.AppendLine(string.Join(",", errors)); + } + + return sb.ToString(); + } + } +} diff --git a/src/Server/OneTrueError.Web/Models/Account/AcceptViewModel.cs b/src/Server/Coderr.Server.WebSite/Models/Accounts/AcceptViewModel.cs similarity index 78% rename from src/Server/OneTrueError.Web/Models/Account/AcceptViewModel.cs rename to src/Server/Coderr.Server.WebSite/Models/Accounts/AcceptViewModel.cs index 49844449..2735502f 100644 --- a/src/Server/OneTrueError.Web/Models/Account/AcceptViewModel.cs +++ b/src/Server/Coderr.Server.WebSite/Models/Accounts/AcceptViewModel.cs @@ -1,10 +1,10 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Models.Account -{ - public class AcceptViewModel : RegisterViewModel - { - [Required] - public string InvitationKey { get; set; } - } +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Models.Accounts +{ + public class AcceptViewModel : RegisterViewModel + { + [Required] + public string InvitationKey { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Models/Account/IsTrueAttribute.cs b/src/Server/Coderr.Server.WebSite/Models/Accounts/IsTrueAttribute.cs similarity index 93% rename from src/Server/OneTrueError.Web/Models/Account/IsTrueAttribute.cs rename to src/Server/Coderr.Server.WebSite/Models/Accounts/IsTrueAttribute.cs index 372533eb..75e37855 100644 --- a/src/Server/OneTrueError.Web/Models/Account/IsTrueAttribute.cs +++ b/src/Server/Coderr.Server.WebSite/Models/Accounts/IsTrueAttribute.cs @@ -1,31 +1,31 @@ -using System; -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Models.Account -{ - public class IsTrueAttribute : ValidationAttribute - { - #region Overrides of ValidationAttribute - - /// - /// Determines whether the specified value of the object is valid. - /// - /// - /// true if the specified value is valid; otherwise, false. - /// - /// - /// The value of the specified validation object on which the - /// is declared. - /// - public override bool IsValid(object value) - { - if (value == null) return false; - if (value.GetType() != typeof(bool)) - throw new InvalidOperationException("can only be used on boolean properties."); - - return (bool) value; - } - - #endregion - } +using System; +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Models.Accounts +{ + public class IsTrueAttribute : ValidationAttribute + { + #region Overrides of ValidationAttribute + + /// + /// Determines whether the specified value of the object is valid. + /// + /// + /// true if the specified value is valid; otherwise, false. + /// + /// + /// The value of the specified validation object on which the + /// is declared. + /// + public override bool IsValid(object value) + { + if (value == null) return false; + if (value.GetType() != typeof(bool)) + throw new InvalidOperationException("can only be used on boolean properties."); + + return (bool) value; + } + + #endregion + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Models/Accounts/LoginResult.cs b/src/Server/Coderr.Server.WebSite/Models/Accounts/LoginResult.cs new file mode 100644 index 00000000..be7ec809 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Models/Accounts/LoginResult.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Coderr.Server.WebSite.Models.Accounts +{ + public class LoginResult + { + public string JwtToken { get; set; } + public string ErrorMessage { get; set; } + public bool Success { get; set; } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Models/Accounts/LoginViewmodel.cs b/src/Server/Coderr.Server.WebSite/Models/Accounts/LoginViewmodel.cs new file mode 100644 index 00000000..8ba3ab99 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Models/Accounts/LoginViewmodel.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Models.Accounts +{ + public class LoginViewModel + { + [Required] + public string Password { get; set; } + + [Required] + public string UserName { get; set; } + + /// + /// Allow new users to register. (view only) + /// + public bool AllowRegistrations { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Models/Accounts/RegisterResult.cs b/src/Server/Coderr.Server.WebSite/Models/Accounts/RegisterResult.cs new file mode 100644 index 00000000..e0634715 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Models/Accounts/RegisterResult.cs @@ -0,0 +1,9 @@ +namespace Coderr.Server.WebSite.Models.Accounts +{ + public class RegisterResult + { + public bool VerificationIsRequested { get; set; } + public bool Success { get; set; } + public string ErrorMessage { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Models/Account/RegisterViewModel.cs b/src/Server/Coderr.Server.WebSite/Models/Accounts/RegisterViewModel.cs similarity index 82% rename from src/Server/OneTrueError.Web/Models/Account/RegisterViewModel.cs rename to src/Server/Coderr.Server.WebSite/Models/Accounts/RegisterViewModel.cs index 4892ec26..9e597398 100644 --- a/src/Server/OneTrueError.Web/Models/Account/RegisterViewModel.cs +++ b/src/Server/Coderr.Server.WebSite/Models/Accounts/RegisterViewModel.cs @@ -1,29 +1,31 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Models.Account -{ - public class RegisterViewModel - { - [Required, Display(Description = "Used to send notification and password resets.")] - public string Email { get; set; } - - - [Display(Name = "First name")] - public string FirstName { get; set; } - - [Display(Name = "Last name")] - public string LastName { get; set; } - - [Required, Compare("Password2")] - public string Password { get; set; } - - [Display(Name = "Retype password")] - public string Password2 { get; set; } - - [Required] - public bool Terms { get; private set; } - - [Required] - public string UserName { get; set; } - } +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Models.Accounts +{ + public class RegisterViewModel : LoginResult + { + [Required, Display(Description = "Used to send notification and password resets.")] + public string Email { get; set; } + + + [Display(Name = "First name")] + public string FirstName { get; set; } + + [Display(Name = "Last name")] + public string LastName { get; set; } + + [Required, Compare("Password2")] + public string Password { get; set; } + + [Display(Name = "Retype password")] + public string Password2 { get; set; } + + [Required] + public bool Terms { get; private set; } + + [Required] + public string UserName { get; set; } + + public string ReturnUrl { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Models/Account/RequestPasswordResetViewModel.cs b/src/Server/Coderr.Server.WebSite/Models/Accounts/RequestPasswordResetViewModel.cs similarity index 77% rename from src/Server/OneTrueError.Web/Models/Account/RequestPasswordResetViewModel.cs rename to src/Server/Coderr.Server.WebSite/Models/Accounts/RequestPasswordResetViewModel.cs index 8f9b8049..f7f6c2c8 100644 --- a/src/Server/OneTrueError.Web/Models/Account/RequestPasswordResetViewModel.cs +++ b/src/Server/Coderr.Server.WebSite/Models/Accounts/RequestPasswordResetViewModel.cs @@ -1,10 +1,10 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Models.Account -{ - public class RequestPasswordResetViewModel - { - [Required] - public string EmailAddress { get; set; } - } +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Models.Accounts +{ + public class RequestPasswordResetViewModel + { + [Required] + public string EmailAddress { get; set; } + } } \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Models/Account/ResetPasswordViewModel.cs b/src/Server/Coderr.Server.WebSite/Models/Accounts/ResetPasswordViewModel.cs similarity index 75% rename from src/Server/OneTrueError.Web/Models/Account/ResetPasswordViewModel.cs rename to src/Server/Coderr.Server.WebSite/Models/Accounts/ResetPasswordViewModel.cs index c6e05855..61a20ce8 100644 --- a/src/Server/OneTrueError.Web/Models/Account/ResetPasswordViewModel.cs +++ b/src/Server/Coderr.Server.WebSite/Models/Accounts/ResetPasswordViewModel.cs @@ -1,16 +1,16 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Models.Account -{ - public class ResetPasswordViewModel - { - [Required] - public string ActivationKey { get; set; } - - [Required, Compare("Password2")] - public string Password { get; set; } - - [Display(Name = "Retype password")] - public string Password2 { get; set; } - } +using System.ComponentModel.DataAnnotations; + +namespace Coderr.Server.WebSite.Models.Accounts +{ + public class ResetPasswordViewModel + { + [Required] + public string ActivationKey { get; set; } + + [Required, Compare("Password2")] + public string Password { get; set; } + + [Display(Name = "Retype password"), Required] + public string Password2 { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Models/ApplicationUser.cs b/src/Server/Coderr.Server.WebSite/Models/ApplicationUser.cs new file mode 100644 index 00000000..f8f2912a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Models/ApplicationUser.cs @@ -0,0 +1,8 @@ +using Microsoft.AspNetCore.Identity; + +namespace Coderr.Server.WebSite.Models +{ + public class ApplicationUser : IdentityUser + { + } +} diff --git a/src/Server/Coderr.Server.WebSite/Models/AuthenticatedUser.cs b/src/Server/Coderr.Server.WebSite/Models/AuthenticatedUser.cs new file mode 100644 index 00000000..d2fe01bb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Models/AuthenticatedUser.cs @@ -0,0 +1,22 @@ +#nullable enable +using Coderr.Server.Api.Core.Applications; + +namespace Coderr.Server.WebSite.Models +{ + public class AuthenticatedUser + { + public AuthenticatedUser(int accountId, string? userName) + { + AccountId = accountId; + UserName = userName; + Applications = new ApplicationListItem[0]; + LicenseText = ""; + } + + public int AccountId { get; private set; } + public string? UserName { get; private set; } + public ApplicationListItem[] Applications { get; set; } + public bool IsSysAdmin { get; set; } + public string LicenseText { get; set; } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Models/ErrorMessage.cs b/src/Server/Coderr.Server.WebSite/Models/ErrorMessage.cs new file mode 100644 index 00000000..02e8700b --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Models/ErrorMessage.cs @@ -0,0 +1,12 @@ +namespace Coderr.Server.WebSite.Models +{ + public class ErrorMessage + { + public ErrorMessage(string reasonPhrase) + { + ReasonPhrase = reasonPhrase; + } + + public string ReasonPhrase { get; } + } +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Models/AccountViewModels.cs b/src/Server/Coderr.Server.WebSite/Models/Users/AccountViewModels.cs similarity index 92% rename from src/Server/OneTrueError.Web/Models/AccountViewModels.cs rename to src/Server/Coderr.Server.WebSite/Models/Users/AccountViewModels.cs index 49b5e2d8..8c903505 100644 --- a/src/Server/OneTrueError.Web/Models/AccountViewModels.cs +++ b/src/Server/Coderr.Server.WebSite/Models/Users/AccountViewModels.cs @@ -1,41 +1,41 @@ -using System.Collections.Generic; - -namespace OneTrueError.Web.Models -{ - // Models returned by AccountController actions. - - public class ExternalLoginViewModel - { - public string Name { get; set; } - - public string State { get; set; } - - public string Url { get; set; } - } - - public class ManageInfoViewModel - { - public string Email { get; set; } - - public IEnumerable ExternalLoginProviders { get; set; } - public string LocalLoginProvider { get; set; } - - public IEnumerable Logins { get; set; } - } - - public class UserInfoViewModel - { - public string Email { get; set; } - - public bool HasRegistered { get; set; } - - public string LoginProvider { get; set; } - } - - public class UserLoginInfoViewModel - { - public string LoginProvider { get; set; } - - public string ProviderKey { get; set; } - } +using System.Collections.Generic; + +namespace Coderr.Server.WebSite.Models.Users +{ + // Models returned by AccountController actions. + + public class ExternalLoginViewModel + { + public string Name { get; set; } + + public string State { get; set; } + + public string Url { get; set; } + } + + public class ManageInfoViewModel + { + public string Email { get; set; } + + public IEnumerable ExternalLoginProviders { get; set; } + public string LocalLoginProvider { get; set; } + + public IEnumerable Logins { get; set; } + } + + public class UserInfoViewModel + { + public string Email { get; set; } + + public bool HasRegistered { get; set; } + + public string LoginProvider { get; set; } + } + + public class UserLoginInfoViewModel + { + public string LoginProvider { get; set; } + + public string ProviderKey { get; set; } + } } \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Models/Users/AuthenticatedUser.cs b/src/Server/Coderr.Server.WebSite/Models/Users/AuthenticatedUser.cs new file mode 100644 index 00000000..7a9a1946 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Models/Users/AuthenticatedUser.cs @@ -0,0 +1,14 @@ +using Coderr.Server.Api.Core.Applications; + +namespace Coderr.Server.WebSite.Models.Users +{ + public class AuthenticatedUser + { + public int AccountId { get; set; } + public string UserName { get; set; } + public ApplicationListItem[] Applications { get; set; } + + public bool IsSysAdmin { get; set; } + public string LicenseText { get; set; } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Program.cs b/src/Server/Coderr.Server.WebSite/Program.cs new file mode 100644 index 00000000..f2954f5f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Program.cs @@ -0,0 +1,86 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Coderr.Client; +using Coderr.Server.Abstractions; +using Coderr.Server.SqlServer.ReportAnalyzer; +using Coderr.Server.WebSite.Infrastructure.Adapters.Logging; +using Griffin.Data.Mapper; +using log4net; +using log4net.Config; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Coderr.Server.WebSite +{ + public class Program + { + private static ILog _logger; + private static string _environmentName; + + public static void Main(string[] args) + { + AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; + + _environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")?.ToLower() ?? "Production"; + + ConfigureLog4Net(); + Err.Configuration.ReportSlowRequests(TimeSpan.FromSeconds(1)); + Err.Configuration.AttachUserPrincipalToken(); + Err.Configuration.CatchLog4NetExceptions(); + + DotNetCqs.LogConfiguration.LogFactory = new MicrosoftLogFactoryAdapter(); + + var provider = new AssemblyScanningMappingProvider(); + provider.Scan(typeof(AnalyticsRepository).Assembly); + EntityMappingProvider.Provider = provider; + + + CreateHostBuilder(args).Build().Run(); + } + + private static void OnUnhandledException(object sender, UnhandledExceptionEventArgs e) + { + _logger.Fatal("Unhandled ", (Exception)e.ExceptionObject); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .UseEnvironment(_environmentName) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }) + .UseEnvironment(_environmentName); + + + private static void ConfigureLog4Net() + { + string logPath; + if (string.IsNullOrEmpty(_environmentName)) + { + logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config"); + } + else + { + logPath = $"log4net.{_environmentName}.config"; + if (!File.Exists(logPath)) + { + logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"log4net.{_environmentName}.config"); + if (!File.Exists(logPath)) + logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log4net.config"); + } + } + + + var repos = LogManager.GetRepository(Assembly.GetEntryAssembly()); + XmlConfigurator.ConfigureAndWatch(repos, new FileInfo(logPath)); + + _logger = LogManager.GetLogger(typeof(Program)); + _logger.Info($"Started {_environmentName} from path {logPath}."); + + + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Properties/launchSettings.json b/src/Server/Coderr.Server.WebSite/Properties/launchSettings.json new file mode 100644 index 00000000..bdf90ca3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Properties/launchSettings.json @@ -0,0 +1,26 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54252", + "sslPort": 44393 + } + }, + "profiles": { + "Debug": { + "commandName": "IISExpress", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Angular": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Properties/serviceDependencies.json b/src/Server/Coderr.Server.WebSite/Properties/serviceDependencies.json new file mode 100644 index 00000000..224ac68c --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Properties/serviceDependencies.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "mssql1": { + "type": "mssql", + "connectionId": "DefaultConnection" + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Properties/serviceDependencies.local.json b/src/Server/Coderr.Server.WebSite/Properties/serviceDependencies.local.json new file mode 100644 index 00000000..48bec3bb --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Properties/serviceDependencies.local.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "mssql1": { + "type": "mssql.local", + "connectionId": "DefaultConnection" + } + } +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Startup.cs b/src/Server/Coderr.Server.WebSite/Startup.cs new file mode 100644 index 00000000..2c8b07f9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Startup.cs @@ -0,0 +1,205 @@ +using System; +using Coderr.Server.Abstractions; +using Coderr.Server.Infrastructure; +using Coderr.Server.SqlServer; +using Coderr.Server.WebSite.Areas.Installation; +using Coderr.Server.WebSite.Infrastructure; +using Coderr.Server.WebSite.Infrastructure.Adapters; +using Coderr.Server.WebSite.Infrastructure.Filters; +using Coderr.Server.WebSite.Infrastructure.WebSockets; +using log4net; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.SpaServices.AngularCli; +using Microsoft.AspNetCore.StaticFiles; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using WebSocketHub = Coderr.Server.WebSite.Hubs.WebSocketHub; + +namespace Coderr.Server.WebSite +{ + public class Startup + { + private readonly CoderrStartup _coderrStartup; + private readonly ILog _logger = LogManager.GetLogger(typeof(Startup)); + + + public Startup(IConfiguration configuration) + { + Configuration = configuration; + _coderrStartup = new CoderrStartup(configuration); + ServerConfig.Instance.Queues.Configure(new ConfigurationWrapper(configuration)); + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + _logger.Debug("ConfigureServices.."); + _coderrStartup.BeginConfigureServices(services); + + EnableCors(services); + + var authBuilder = services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(JwtHelper.Configure); + _coderrStartup.AddAuthentication(authBuilder); + + services.AddControllersWithViews(options => + { + options.Filters.Add(); + options.Filters.Add(); + }); + services.AddRazorPages().AddRazorRuntimeCompilation(); + services.AddSignalR(options => + { + options.EnableDetailedErrors = true; + }); + + _coderrStartup.EndConfigureServices(services); + + services.AddSpaStaticFiles(configuration => + { + configuration.RootPath = "ClientApp/dist"; + }); + } + + bool IsCorsEnabled => Configuration["EnableCors"]?.Equals("true", StringComparison.OrdinalIgnoreCase) == true; + + public bool IsDevelopment(IWebHostEnvironment env) + { + return env.IsDevelopment() || env.EnvironmentName == "onpremisedev"; + } + + private void EnableCors(IServiceCollection services) + { + if (IsCorsEnabled) + { + services.AddCors(o => o.AddPolicy("CorsPolicy", builder => + { + builder.AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + })); + } + else + { + // Add the policy, but do not allow any origins + // which means that the policy is effectively denying everything. + services.AddCors(o => o.AddPolicy("CorsPolicy", builder => { builder.AllowAnyHeader(); })); + } + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApplicationLifetime applicationLifetime) + { + + applicationLifetime.ApplicationStarted.Register(() => + { + _coderrStartup.Start(); + }); + + _logger.Debug("Configure.."); + _coderrStartup.Configure(app, applicationLifetime); + + var webSocketOptions = new WebSocketOptions() + { + KeepAliveInterval = TimeSpan.FromSeconds(120), + }; + app.UseWebSockets(webSocketOptions); + + if (IsCorsEnabled) + { + app.UseCors("CorsPolicy"); + } + + MapWebManifest(app); + + if (IsDevelopment(env)) + { + app.UseDeveloperExceptionPage(); + //app.UseDatabaseErrorPage(); + } + else + { + app.UseExceptionHandler("/Error"); + // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseMiddleware(); + app.UseForwardedHeaders(); + + if (!IsDevelopment(env)) + { + app.UseSpaStaticFiles(); + } + + app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); + _coderrStartup.ConfigureAuthentication(app); + + + //app.Use(async (context, next) => + //{ + // if (context.Request.Path != "/ws") + // { + // await next(); + // return; + // } + + // if (!context.WebSockets.IsWebSocketRequest) + // { + // context.Response.StatusCode = 400; + // return; + // } + + // using var webSocket = await context.WebSockets.AcceptWebSocketAsync(); + // var service = context.RequestServices.GetRequiredService(); + // await service.Process(context.RequestAborted, webSocket, context.User); + //}); + app.UseEndpoints(endpoints => + { + endpoints.MapControllerRoute( + name: "Installation", + pattern: "{area:exists}/{controller=Setup}/{action=Index}/{id?}"); + endpoints.MapControllerRoute( + name: "default", + pattern: "{controller}/{action=Index}/{id?}"); + endpoints.MapHub("/hub"); + endpoints.MapRazorPages(); + }); + + app.UseSpa(spa => + { + // To learn more about options for serving an Angular SPA from ASP.NET Core, + // see https://go.microsoft.com/fwlink/?linkid=864501 + + spa.Options.SourcePath = "ClientApp"; + + if (IsDevelopment(env)) + { + spa.UseAngularCliServer(npmScript: "start"); + } + }); + + _coderrStartup.ConfigureEnd(app); + } + + private static void MapWebManifest(IApplicationBuilder app) + { + var provider = new FileExtensionContentTypeProvider(); + provider.Mappings[".webmanifest"] = "application/manifest+json"; + app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider }); + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/Views/Account/Accept.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/Accept.cshtml new file mode 100644 index 00000000..054efe71 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/Account/Accept.cshtml @@ -0,0 +1,109 @@ +@model Coderr.Server.WebSite.Models.Accounts.AcceptViewModel +@{ + ViewBag.Title = "Register"; +} + +
+
+

Welcome to Coderr

+

+ To accept the invitation you have to fill out the following form. +

+
+
+ @Html.AntiForgeryToken() + @Html.HiddenFor(x => x.InvitationKey) +
+ + @Html.TextBoxFor(model => model.UserName, new { placeholder = "User name", @class = "form-control" }) + @Html.ValidationMessageFor(model => model.UserName) +
+
+ + @Html.PasswordFor(model => model.Password, new { placeholder = "Password", @class = "form-control" }) + @Html.ValidationMessageFor(model => model.Password) + +
+
+ + @Html.PasswordFor(model => model.Password2, new { placeholder = "Password verification", equalTo = "#Password", @class = "form-control" }) + @Html.ValidationMessageFor(model => model.Password2) +
+
+ + @Html.TextBoxFor(model => model.Email, new { placeholder = "Email", @class = "form-control" }) + @Html.ValidationMessageFor(model => model.Email) +
+
+ + @Html.ActionLink("Back to first page", "Index", "Home", null, new { @class = "btn btn-default" }) +
+
+
+
+
+ +@section scripts +{ + +} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Views/Account/Activate.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/Activate.cshtml similarity index 95% rename from src/Server/OneTrueError.Web/Views/Account/Activate.cshtml rename to src/Server/Coderr.Server.WebSite/Views/Account/Activate.cshtml index 529fa25a..84a1fedb 100644 --- a/src/Server/OneTrueError.Web/Views/Account/Activate.cshtml +++ b/src/Server/Coderr.Server.WebSite/Views/Account/Activate.cshtml @@ -1,6 +1,6 @@ -@{ - ViewBag.Title = "Activate"; -} - -

Activate

+@{ + ViewBag.Title = "Activate"; +} + +

Activate

@Html.ValidationSummary() \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Views/Account/ActivationRequested.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/ActivationRequested.cshtml new file mode 100644 index 00000000..7d34d4d6 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/Account/ActivationRequested.cshtml @@ -0,0 +1,6 @@ +@{ + ViewBag.Title = "Activation"; +} + +

Activation required

+
You need to activate your account. Click on the link that was sent to your email account.
diff --git a/src/Server/OneTrueError.Web/Views/Account/ChangePassword.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/ChangePassword.cshtml similarity index 92% rename from src/Server/OneTrueError.Web/Views/Account/ChangePassword.cshtml rename to src/Server/Coderr.Server.WebSite/Views/Account/ChangePassword.cshtml index 9423e985..054bcd4f 100644 --- a/src/Server/OneTrueError.Web/Views/Account/ChangePassword.cshtml +++ b/src/Server/Coderr.Server.WebSite/Views/Account/ChangePassword.cshtml @@ -1,17 +1,17 @@ -@{ - Layout = null; -} - - - - - - - ChangePassword - - -
- -
- +@{ + Layout = null; +} + + + + + + + ChangePassword + + +
+ +
+ \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Views/Account/InvitationNotFound.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/InvitationNotFound.cshtml new file mode 100644 index 00000000..a1e0720f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/Account/InvitationNotFound.cshtml @@ -0,0 +1,27 @@ +@{ + ViewBag.Title = "Account registered"; +} + +

Account registered

+
We have now sent an activation email to your account. Click on the link in it to active your account.
+
We only use your email address to send alerts (configured by you) and to allow you to reset your password.
+ + + + + \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Views/Account/PasswordRequestReceived.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/PasswordRequestReceived.cshtml similarity index 75% rename from src/Server/OneTrueError.Web/Views/Account/PasswordRequestReceived.cshtml rename to src/Server/Coderr.Server.WebSite/Views/Account/PasswordRequestReceived.cshtml index 1371bbcd..91222c3a 100644 --- a/src/Server/OneTrueError.Web/Views/Account/PasswordRequestReceived.cshtml +++ b/src/Server/Coderr.Server.WebSite/Views/Account/PasswordRequestReceived.cshtml @@ -1,13 +1,13 @@ -@model OneTrueError.Web.Models.Account.RequestPasswordResetViewModel - -@{ - ViewBag.Title = "Password reset email sent"; -} - - -
-
-

Email sent

-

You will shortly receive a password reset email (if the specified address exists in our system).

-
+@model Coderr.Server.WebSite.Models.Accounts.RequestPasswordResetViewModel + +@{ + ViewBag.Title = "Password reset email sent"; +} + + +
+
+

Email sent

+

You will shortly receive a password reset email (if the specified address exists in our system).

+
\ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Views/Account/Register.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/Register.cshtml new file mode 100644 index 00000000..6075a6a7 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/Account/Register.cshtml @@ -0,0 +1,126 @@ +@model Coderr.Server.WebSite.Models.Accounts.RegisterViewModel +@{ + ViewBag.Title = "Register account"; +} + + + +
+
+ Account registration +
+
+ + +
+
+
+ @Html.HiddenFor(x => x.ReturnUrl) + @Html.ValidationSummary(true) + @Html.AntiForgeryToken() +
+ + @Html.TextBoxFor(model => model.UserName, new {placeholder = "User name", @class = "form-control"}) + @Html.ValidationMessageFor(model => model.UserName) +
+
+ + @Html.PasswordFor(model => model.Password, new {placeholder = "Password", @class = "form-control"}) + @Html.ValidationMessageFor(model => model.Password) + +
+
+ + @Html.PasswordFor(model => model.Password2, new {placeholder = "Password verification", @class = "form-control"}) + @Html.ValidationMessageFor(model => model.Password2) +
+
+ + @Html.TextBoxFor(model => model.Email, new {placeholder = "Email", @class = "form-control"}) + @Html.ValidationMessageFor(model => model.Email) +
+
+ + Back to login +
+
+
+
+
+
+@section scripts { + +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Views/Account/RequestPasswordReset.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/RequestPasswordReset.cshtml new file mode 100644 index 00000000..f907491a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/Account/RequestPasswordReset.cshtml @@ -0,0 +1,34 @@ +@model Coderr.Server.WebSite.Models.Accounts.RequestPasswordResetViewModel + +@{ + ViewBag.Title = "Request password reset"; +} +
+
+ Reset password +
+
+ + + @using (Html.BeginForm("RequestPasswordReset", "Account", FormMethod.Post, new { @class = "form-horizontal" })) + { + @Html.AntiForgeryToken() + + + Enter your email address to get instructions sent to your inbox. + + @Html.ValidationSummary(true) + +
+ + @Html.ValidationMessageFor(model => model.EmailAddress) +
+
+ + @Html.ActionLink("Back to login", "Login", null, new { @class = "btn" }) + +
+ + } +
+
diff --git a/src/Server/Coderr.Server.WebSite/Views/Account/ResetPassword.cshtml b/src/Server/Coderr.Server.WebSite/Views/Account/ResetPassword.cshtml new file mode 100644 index 00000000..ba7e755e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/Account/ResetPassword.cshtml @@ -0,0 +1,101 @@ +@model Coderr.Server.WebSite.Models.Accounts.ResetPasswordViewModel + +@{ + ViewBag.Title = "Reset password"; +} + +
+
+ Reset password +
+
+
+ @Html.ValidationSummary(true) + @Html.AntiForgeryToken() +

Enter your new password to complete the password reset.

+ +
+ + + @Html.ValidationMessageFor(model => model.Password) + +
+
+ + + @Html.ValidationMessageFor(model => model.Password2) +
+
+ + Cancel +
+ +
+ +
+
+ +@section scripts{ + +} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/Views/Authentication/Login.cshtml b/src/Server/Coderr.Server.WebSite/Views/Authentication/Login.cshtml new file mode 100644 index 00000000..5deb8393 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/Authentication/Login.cshtml @@ -0,0 +1,35 @@ +@using Coderr.Server.Abstractions + + + + + + + + Coderr + + + + + + @RenderBody() + + + + + + + + \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/ote_bootstrap_variables.min.css b/src/Server/Coderr.Server.WebSite/Views/Home/Index.cshtml similarity index 100% rename from src/Server/OneTrueError.Web/Content/ote_bootstrap_variables.min.css rename to src/Server/Coderr.Server.WebSite/Views/Home/Index.cshtml diff --git a/src/Server/Coderr.Server.WebSite/Views/Shared/_Layout.cshtml b/src/Server/Coderr.Server.WebSite/Views/Shared/_Layout.cshtml new file mode 100644 index 00000000..3b08937e --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/Shared/_Layout.cshtml @@ -0,0 +1,57 @@ +@using Coderr.Server.Abstractions + + + + + + + + Coderr + + @RenderSection("styles", false) + + + + + + +
+ @RenderBody() +
+ + + + @RenderSection("scripts", false) + + + + + diff --git a/src/Server/Coderr.Server.WebSite/Views/_ViewImports.cshtml b/src/Server/Coderr.Server.WebSite/Views/_ViewImports.cshtml new file mode 100644 index 00000000..9ec2efc9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using Microsoft.AspNetCore.Identity +@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers" diff --git a/src/Server/Coderr.Server.WebSite/Views/_ViewStart.cshtml b/src/Server/Coderr.Server.WebSite/Views/_ViewStart.cshtml new file mode 100644 index 00000000..a5f10045 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Server/Coderr.Server.WebSite/appsettings.Development.json b/src/Server/Coderr.Server.WebSite/appsettings.Development.json new file mode 100644 index 00000000..2d1b8f79 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Debug", + "Microsoft": "Debug" + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/appsettings.Publish.json b/src/Server/Coderr.Server.WebSite/appsettings.Publish.json new file mode 100644 index 00000000..a1fe5cce --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/appsettings.Publish.json @@ -0,0 +1,19 @@ +{ + "EnableCors": true, + "ConnectionStrings": { + "Db": "Data Source=.;Initial Catalog=Coderr;Integrated Security=True;Connect Timeout=15;" + }, + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/appsettings.Test.json b/src/Server/Coderr.Server.WebSite/appsettings.Test.json new file mode 100644 index 00000000..410a44ca --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/appsettings.Test.json @@ -0,0 +1,24 @@ +{ + "ConnectionStrings": { + "Db": "Server=tcp:live-test-dbserver.database.windows.net,1433;Initial Catalog=live-test-global;Persist Security Info=False;User ID=liveadmin;Password=s0methingEz;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;", + "Tenant": "Server=tcp:live-test-dbserver.database.windows.net,1433;Initial Catalog={databaseName};Persist Security Info=False;User ID=liveadmin;Password=s0methingEz;MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" + }, + "DbSettings": { + "TenantDbNameTemplate": "live-test-tenant{0}" + }, + "DisabledModules": { + "App": [ + "BackgroundJobs", + "DbConnectionConfig" + ], + "Reportanalyzer": [] + }, + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/appsettings.json b/src/Server/Coderr.Server.WebSite/appsettings.json new file mode 100644 index 00000000..e55578c9 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/appsettings.json @@ -0,0 +1,28 @@ +{ + "General": { + "IsDemo": true, + "UsePostmark": true, + "InstallationPassword": "GoForIt" + }, + "EnableCors": true, + "ConnectionStrings": { + "Db": "Data Source=.;Initial Catalog=CoderrPremise;Integrated Security=True;Connect Timeout=15;" + }, + //"Queues": { + // "Type": "Disk", + // "Folder": "D:\\CoderrData" + //}, + + "Logging": { + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +} diff --git a/src/Server/Coderr.Server.WebSite/compilenpm.cmd b/src/Server/Coderr.Server.WebSite/compilenpm.cmd new file mode 100644 index 00000000..fe4e080a --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/compilenpm.cmd @@ -0,0 +1 @@ +node node_modules/webpack/bin/webpack.js --config webpack.config.prod.js --mode=production \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/log4net.config b/src/Server/Coderr.Server.WebSite/log4net.config new file mode 100644 index 00000000..4650cb5f --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/log4net.config @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/log4net.prod.config b/src/Server/Coderr.Server.WebSite/log4net.prod.config new file mode 100644 index 00000000..32cedbcf --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/log4net.prod.config @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/log4net.production.config b/src/Server/Coderr.Server.WebSite/log4net.production.config new file mode 100644 index 00000000..cbb4c1ff --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/log4net.production.config @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/log4net.test.config b/src/Server/Coderr.Server.WebSite/log4net.test.config new file mode 100644 index 00000000..c9773418 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/log4net.test.config @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/publish_clean.cmd b/src/Server/Coderr.Server.WebSite/publish_clean.cmd new file mode 100644 index 00000000..0d1f5890 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/publish_clean.cmd @@ -0,0 +1,26 @@ +IF EXIST "bin\Release\netcoreapp3.1\publish\appsettings.Development.json" ( + del /q bin\Release\netcoreapp3.1\publish\appsettings.json + del /q bin\Release\netcoreapp3.1\publish\appsettings.Development.json + del /q bin\Release\netcoreapp3.1\publish\appsettings.Test.json + del /q bin\Release\netcoreapp3.1\publish\appsettings.OnPremiseDev.json + del /q bin\Release\netcoreapp3.1\publish\appsettings.Production.json + move bin\Release\netcoreapp3.1\publish\appsettings.Publish.json bin\Release\netcoreapp3.1\publish\appsettings.json +) + +del /q bin\Release\netcoreapp3.1\publish\log4net.production.config +del /q bin\Release\netcoreapp3.1\publish\log4net.test.config +del /q bin\Release\netcoreapp3.1\publish\*.pdb + +rmdir /q /s bin\Release\netcoreapp3.1\publish\de +rmdir /q /s bin\Release\netcoreapp3.1\publish\es +rmdir /q /s bin\Release\netcoreapp3.1\publish\fr +rmdir /q /s bin\Release\netcoreapp3.1\publish\it +rmdir /q /s bin\Release\netcoreapp3.1\publish\ja +rmdir /q /s bin\Release\netcoreapp3.1\publish\ko +rmdir /q /s bin\Release\netcoreapp3.1\publish\ru +rmdir /q /s bin\Release\netcoreapp3.1\publish\zh-Hans +rmdir /q /s bin\Release\netcoreapp3.1\publish\zh-Hant +rmdir /q /s bin\Release\netcoreapp3.1\publish\cs +rmdir /q /s bin\Release\netcoreapp3.1\publish\pl +rmdir /q /s bin\Release\netcoreapp3.1\publish\pt-BR +rmdir /q /s bin\Release\netcoreapp3.1\publish\tr diff --git a/src/Server/Coderr.Server.WebSite/publish_upgrade.cmd b/src/Server/Coderr.Server.WebSite/publish_upgrade.cmd new file mode 100644 index 00000000..a7e257df --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/publish_upgrade.cmd @@ -0,0 +1,5 @@ +@echo off +call publish_clean.cmd +del /q bin\Release\PublishOutput\log4net*.config +del /q bin\Release\PublishOutput\appsettings*.json +del /q bin\Release\PublishOutput\web.config diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/favicon-16x16.png b/src/Server/Coderr.Server.WebSite/wwwroot/favicon-16x16.png new file mode 100644 index 00000000..be4a1fc0 Binary files /dev/null and b/src/Server/Coderr.Server.WebSite/wwwroot/favicon-16x16.png differ diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/favicon-32x32.png b/src/Server/Coderr.Server.WebSite/wwwroot/favicon-32x32.png new file mode 100644 index 00000000..e746332e Binary files /dev/null and b/src/Server/Coderr.Server.WebSite/wwwroot/favicon-32x32.png differ diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/favicon-96x96.png b/src/Server/Coderr.Server.WebSite/wwwroot/favicon-96x96.png new file mode 100644 index 00000000..12c064c6 Binary files /dev/null and b/src/Server/Coderr.Server.WebSite/wwwroot/favicon-96x96.png differ diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/favicon.ico b/src/Server/Coderr.Server.WebSite/wwwroot/favicon.ico new file mode 100644 index 00000000..8e9e27e5 Binary files /dev/null and b/src/Server/Coderr.Server.WebSite/wwwroot/favicon.ico differ diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/images/Spinner-1s-200px.svg b/src/Server/Coderr.Server.WebSite/wwwroot/images/Spinner-1s-200px.svg new file mode 100644 index 00000000..b39b3776 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/wwwroot/images/Spinner-1s-200px.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/images/navico.png b/src/Server/Coderr.Server.WebSite/wwwroot/images/navico.png new file mode 100644 index 00000000..d5b94553 Binary files /dev/null and b/src/Server/Coderr.Server.WebSite/wwwroot/images/navico.png differ diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/service-worker.js b/src/Server/Coderr.Server.WebSite/wwwroot/service-worker.js new file mode 100644 index 00000000..5a532bbf --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/wwwroot/service-worker.js @@ -0,0 +1,82 @@ +'use strict'; + +self.addEventListener('install', function (event) { + self.skipWaiting(); +}); + +self.addEventListener('activate', function (event) { + event.waitUntil(clients.claim()); +}); + +// Respond to a server push with a user notification +self.addEventListener('push', function (event) { + if (event.data) { + const { title, lang = 'en', badge, body, tag, timestamp, requireInteraction, actions, image, data } = event.data.json(); + + const promiseChain = self.registration.showNotification(title, { + lang, + body, + data, + requireInteraction, + tag: tag || undefined, + timestamp: timestamp ? Date.parse(timestamp) : undefined, + actions: actions || undefined, + image: image || undefined, + icon: '/favicon-32x32.png', + badge: badge || undefined + }); + + // Ensure the toast notification is displayed before exiting this function + event.waitUntil(promiseChain); + } +}); + +self.addEventListener('notificationclick', function (event) { + var notification = event.notification; + var action = event.action; + notification.close(); + + // When data have the url attached, just visit it. + var storedUrl = notification.data[action + 'Url']; + if (storedUrl) { + clients.openWindow(storedUrl); + return; + } + + var url = '/discover/incidents/' + notification.data.applicationId + '/incident/' + notification.data.incidentId; + if (action === 'AssignToMe') { + url = '/discover/assign/incident/' + notification.data.incidentId; + } + + clients.openWindow(url); + //event.waitUntil( + // clients.matchAll({ type: 'window', includeUncontrolled: true }) + // .then(function (clientList) { + // if (clientList.length > 0) { + // let client = clientList[0]; + + // for (let i = 0; i < clientList.length; i++) { + // if (clientList[i].focused) { + // client = clientList[i]; + // } + // } + + // return client.focus(); + // } + + + // console.log('clicked', event); + // return clients.openWindow('/'); + // }) + //); +}); + +self.addEventListener('pushsubscriptionchange', function (event) { + event.waitUntil( + Promise.all([ + Promise.resolve(event.oldSubscription ? deleteSubscription(event.oldSubscription) : true), + Promise.resolve(event.newSubscription ? event.newSubscription : subscribePush(registration)) + .then(function (sub) { return saveSubscription(sub); }) + ]) + ); +}); diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/styles.0833d9396d7aae04dcd9.css b/src/Server/Coderr.Server.WebSite/wwwroot/styles.0833d9396d7aae04dcd9.css new file mode 100644 index 00000000..f714a0b3 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/wwwroot/styles.0833d9396d7aae04dcd9.css @@ -0,0 +1 @@ +html{box-sizing:border-box;font-size:15px;font-family:Sofia Pro,sans-serif;height:100%}*,:after,:before{box-sizing:inherit}body,h1,h2,h3,h4,h5,h6,ol,p,ul{margin:0;padding:0;font-weight:400}ol,ul{list-style:none}img{max-width:100%;height:auto}.card input[type=radio]{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;outline:0;background:#eee;height:16px;width:16px;border:1px solid #fff}.card input[type=radio]:checked{background:#59c1d5;color:#59c1d5!important}.card input[type=radio]:hover{filter:brightness(90%)}.card input[type=radio]:disabled{background:#393938;opacity:.6;pointer-events:none}.card input[type=radio]:after{content:"";position:relative;left:40%;top:20%;width:15%;height:40%;display:none}.card input[type=radio]:checked:after{display:block}.card input[type=radio]:disabled:after{border-color:#f4f4f4}.bg.white{background-color:#fff}.bg.dark{background-color:#393938}.alert{padding:10px}.alert.warning{background-color:#f18c65;border:1px solid #ed6936;color:#ededed}.alert.success{background-color:#d8eef4;border:1px solid #b0dde9}.card input[type=checkbox]{cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;outline:0;background:#eee;height:16px;width:16px;border:1px solid #fff}.card input[type=checkbox]:checked{background:#59c1d5;color:#59c1d5!important}.card input[type=checkbox]:hover{filter:brightness(90%)}.card input[type=checkbox]:disabled{background:#393938;opacity:.6;pointer-events:none}.card input[type=checkbox]:after{content:"";position:relative;left:40%;top:20%;width:15%;height:40%;display:none}.card input[type=checkbox]:checked:after{display:block}.card input[type=checkbox]:disabled:after{border-color:#f4f4f4}input:invalid{box-shadow:0 0 5px 1px #f18c65}input:focus:invalid{box-shadow:none}input,select,textarea{padding:4px;border:1px solid #404040;border-radius:3px}.form .form-group{margin-top:15px}.form label{display:block;font-weight:700}.form input.inline,.form label.inline{display:inline}.form input[type=email],.form input[type=number],.form input[type=password],.form input[type=text],.form select,.form textarea{background-color:#abdbe7;border-radius:3px;padding:8px;border:#5cb9d0;width:100%}.form textarea{min-height:100px;width:100%}.form button{display:inline-flex}.form a.submit,.form button[type=submit]{background-color:#59c1d5;border:#288ca0;color:#ededed;width:inherit}.form a.reset,.form button[type=reset]{color:#ededed;background-color:#33b0c8;border:#dc4c14}.row{display:flex;width:100%;flex-flow:row wrap;max-width:100%}.container{display:block;max-width:1400px;margin:0 auto;padding:-10px}.container h1{font-size:48px}.col{justify-content:center;align-items:start;flex-basis:0;flex:1 1 200px;margin:10px;min-width:0}.col-2{flex:2}.col-3{flex:3}.hidden{display:none;opacity:0}.w-100{width:100%}@media only screen and (max-width: 600px){.col{display:block;width:100%}}.y-center{align-items:center}.x-center{justify-content:center}.mx-auto{margin:0 auto}.mt-0{margin-top:0}.mb-0{margin-bottom:0}.ml-0{margin-left:0!important}.m-0{margin:0}.pt-0{padding-top:0}.pl-0{padding-left:0}.pr-0{padding-right:0}.pb-0{padding-bottom:0}.p-0{padding:0}.mt-1{margin-top:5px}.mb-1{margin-bottom:5px}.ml-1{margin-left:5px!important}.m-1{margin:5px}.pt-1{padding-top:5px}.pl-1{padding-left:5px}.pr-1{padding-right:5px}.pb-1{padding-bottom:5px}.p-1{padding:5px}.mt-2{margin-top:10px}.mb-2{margin-bottom:10px}.ml-2{margin-left:10px!important}.m-2{margin:10px}.pt-2{padding-top:10px}.pl-2{padding-left:10px}.pr-2{padding-right:10px}.pb-2{padding-bottom:10px}.p-2{padding:10px}.mt-3{margin-top:15px}.mb-3{margin-bottom:15px}.ml-3{margin-left:15px!important}.m-3{margin:15px}.pt-3{padding-top:15px}.pl-3{padding-left:15px}.pr-3{padding-right:15px}.pb-3{padding-bottom:15px}.p-3{padding:15px}.mt-4{margin-top:20px}.mb-4{margin-bottom:20px}.ml-4{margin-left:20px!important}.m-4{margin:20px}.pt-4{padding-top:20px}.pl-4{padding-left:20px}.pr-4{padding-right:20px}.pb-4{padding-bottom:20px}.p-4{padding:20px}.mt-5{margin-top:25px}.mb-5{margin-bottom:25px}.ml-5{margin-left:25px!important}.m-5{margin:25px}.pt-5{padding-top:25px}.pl-5{padding-left:25px}.pr-5{padding-right:25px}.pb-5{padding-bottom:25px}.p-5{padding:25px}.panel .btn,.panel a.reset,.panel a.submit,.panel button[type=reset],.panel button[type=submit]{background-color:#ededed;color:#393938;display:inline-flex}.panel.fill .btn,.panel .fill .btn,.panel.fill a.reset,.panel .fill a.reset,.panel.fill a.submit,.panel .fill a.submit,.panel.fill button[type=reset],.panel .fill button[type=reset],.panel.fill button[type=submit],.panel .fill button[type=submit]{background-color:#59c1d5;border-color:#59c1d5;color:#ededed;display:inline-flex}.panel a.submit,.panel button[type=submit]{background-color:#ededed;border:#d4d4d4;color:#393938;width:inherit}.panel a.reset,.panel button[type=reset]{color:#ededed;background-color:#f18c65;border:#ed6936}.form a.reset,.form a.submit,.form button[type=reset],.form button[type=submit],.panel a.reset,.panel a.submit,.panel button[type=reset],.panel button[type=submit],a.btn,button.btn,input.button{padding:10px 12px;margin-top:15px;margin-right:5px;border-radius:3px;text-decoration:none;box-shadow:0 8px 15px rgba(0,0,0,.1);text-shadow:1px 1px rgba(0,0,0,.05);text-align:center;cursor:pointer;display:inline-block}.form a.small.reset,.form a.small.submit,.form button.small[type=reset],.form button.small[type=submit],.panel a.small.reset,.panel a.small.submit,.panel button.small[type=reset],.panel button.small[type=submit],a.btn.small,button.btn.small,input.button.small{margin-top:5px;margin-right:2px;padding:4px 6px;font-size:.9em}.form a.default.reset,.form a.default.submit,.form button.default[type=reset],.form button.default[type=submit],.panel a.default.reset,.panel a.default.submit,.panel button.default[type=reset],.panel button.default[type=submit],a.btn.default,button.btn.default,input.button.default{background:#fff;color:#393938}.form a.block.reset,.form a.block.submit,.form button.block[type=reset],.form button.block[type=submit],.panel a.block.reset,.panel a.block.submit,.panel button.block[type=reset],.panel button.block[type=submit],a.btn.block,button.btn.block,input.button.block{display:block;width:100%}.form a.light.reset,.form a.light.submit,.form button.light[type=reset],.form button.light[type=submit],.panel a.light.reset,.panel a.light.submit,.panel button.light[type=reset],.panel button.light[type=submit],a.btn.light,button.btn.light,input.button.light{background-color:#ededed;color:#393938}.form a.dark.reset,.form a.dark.submit,.form button.dark[type=reset],.form button.dark[type=submit],.panel a.dark.reset,.panel a.dark.submit,.panel button.dark[type=reset],.panel button.dark[type=submit],a.btn.dark,button.btn.dark,input.button.dark{background-color:#393938}.form a.red.reset,.form a.red.submit,.form button.red[type=reset],.form button.red[type=submit],.panel a.red.reset,.panel a.red.submit,.panel button.red[type=reset],.panel button.red[type=submit],a.btn.red,button.btn.red,input.button.red{background-color:#f18c65!important;color:#fff;border:1px solid #f18960;transition:.5s}.form a.red.reset:hover,.form a.red.submit:hover,.form button.red[type=reset]:hover,.form button.red[type=submit]:hover,.panel a.red.reset:hover,.panel a.red.submit:hover,.panel button.red[type=reset]:hover,.panel button.red[type=submit]:hover,a.btn.red:hover,button.btn.red:hover,input.button.red:hover{background-color:#eb581f!important}.form a.blue.reset,.form a.blue.submit,.form a.default.reset,.form a.default.submit,.form button.blue[type=reset],.form button.blue[type=submit],.form button.default[type=reset],.form button.default[type=submit],.panel a.blue.reset,.panel a.blue.submit,.panel a.default.reset,.panel a.default.submit,.panel button.blue[type=reset],.panel button.blue[type=submit],.panel button.default[type=reset],.panel button.default[type=submit],a.btn.blue,a.btn.default,button.btn.blue,button.btn.default,input.button.blue,input.button.default{background-color:#59c1d5!important;color:#fff;border:1px solid #55bfd4}.form a.red50.reset,.form a.red50.submit,.form button.red50[type=reset],.form button.red50[type=submit],.panel a.red50.reset,.panel a.red50.submit,.panel button.red50[type=reset],.panel button.red50[type=submit],a.btn.red50,button.btn.red50,input.button.red50{background-color:hsla(17,83%,67%,.75)!important;color:#fff;border:1px solid hsla(17,83%,67%,.95)}.form a.red-2.reset,.form a.red-2.submit,.form button.red-2[type=reset],.form button.red-2[type=submit],.panel a.red-2.reset,.panel a.red-2.submit,.panel button.red-2[type=reset],.panel button.red-2[type=submit],a.btn.red-2,button.btn.red-2,input.button.red-2{background-color:#f39d7c!important;color:#fff;border:1px solid hsla(17,83%,67%,.5)}.form a.white.reset,.form a.white.submit,.form button.white[type=reset],.form button.white[type=submit],.panel a.white.reset,.panel a.white.submit,.panel button.white[type=reset],.panel button.white[type=submit],a.btn.white,button.btn.white,input.button.white{background-color:#fff!important;color:#2e2d2c;border:1px solid #fcfcfc}.form a.gray.reset,.form a.gray.submit,.form button.gray[type=reset],.form button.gray[type=submit],.panel a.gray.reset,.panel a.gray.submit,.panel button.gray[type=reset],.panel button.gray[type=submit],a.btn.gray,button.btn.gray,input.button.gray{background-color:#eee!important;color:#141414;border:1px solid #fcfcfc}.form .panel a.gray.reset a.reset,.form .panel a.gray.reset a.submit,.form .panel a.gray.reset button[type=reset],.form .panel a.gray.reset button[type=submit],.form .panel a.gray.submit a.reset,.form .panel a.gray.submit a.submit,.form .panel a.gray.submit button[type=reset],.form .panel a.gray.submit button[type=submit],.form .panel button.gray[type=reset] a.reset,.form .panel button.gray[type=reset] a.submit,.form .panel button.gray[type=reset] button[type=reset],.form .panel button.gray[type=reset] button[type=submit],.form .panel button.gray[type=submit] a.reset,.form .panel button.gray[type=submit] a.submit,.form .panel button.gray[type=submit] button[type=reset],.form .panel button.gray[type=submit] button[type=submit],.form a.btn.gray a.reset,.form a.btn.gray a.submit,.form a.btn.gray button[type=reset],.form a.btn.gray button[type=submit],.form a.gray.reset .btn,.form a.gray.reset .panel a.reset,.form a.gray.reset .panel a.submit,.form a.gray.reset .panel button[type=reset],.form a.gray.reset .panel button[type=submit],.form a.gray.reset a.reset,.form a.gray.reset a.submit,.form a.gray.reset button[type=reset],.form a.gray.reset button[type=submit],.form a.gray.submit .btn,.form a.gray.submit .panel a.reset,.form a.gray.submit .panel a.submit,.form a.gray.submit .panel button[type=reset],.form a.gray.submit .panel button[type=submit],.form a.gray.submit a.reset,.form a.gray.submit a.submit,.form a.gray.submit button[type=reset],.form a.gray.submit button[type=submit],.form button.btn.gray a.reset,.form button.btn.gray a.submit,.form button.btn.gray button[type=reset],.form button.btn.gray button[type=submit],.form button.gray[type=reset] .btn,.form button.gray[type=reset] .panel a.reset,.form button.gray[type=reset] .panel a.submit,.form button.gray[type=reset] .panel button[type=reset],.form button.gray[type=reset] .panel button[type=submit],.form button.gray[type=reset] a.reset,.form button.gray[type=reset] a.submit,.form button.gray[type=reset] button[type=reset],.form button.gray[type=reset] button[type=submit],.form button.gray[type=submit] .btn,.form button.gray[type=submit] .panel a.reset,.form button.gray[type=submit] .panel a.submit,.form button.gray[type=submit] .panel button[type=reset],.form button.gray[type=submit] .panel button[type=submit],.form button.gray[type=submit] a.reset,.form button.gray[type=submit] a.submit,.form button.gray[type=submit] button[type=reset],.form button.gray[type=submit] button[type=submit],.form input.button.gray a.reset,.form input.button.gray a.submit,.form input.button.gray button[type=reset],.form input.button.gray button[type=submit],.panel .form a.gray.reset a.reset,.panel .form a.gray.reset a.submit,.panel .form a.gray.reset button[type=reset],.panel .form a.gray.reset button[type=submit],.panel .form a.gray.submit a.reset,.panel .form a.gray.submit a.submit,.panel .form a.gray.submit button[type=reset],.panel .form a.gray.submit button[type=submit],.panel .form button.gray[type=reset] a.reset,.panel .form button.gray[type=reset] a.submit,.panel .form button.gray[type=reset] button[type=reset],.panel .form button.gray[type=reset] button[type=submit],.panel .form button.gray[type=submit] a.reset,.panel .form button.gray[type=submit] a.submit,.panel .form button.gray[type=submit] button[type=reset],.panel .form button.gray[type=submit] button[type=submit],.panel a.btn.gray a.reset,.panel a.btn.gray a.submit,.panel a.btn.gray button[type=reset],.panel a.btn.gray button[type=submit],.panel a.gray.reset .btn,.panel a.gray.reset .form a.reset,.panel a.gray.reset .form a.submit,.panel a.gray.reset .form button[type=reset],.panel a.gray.reset .form button[type=submit],.panel a.gray.reset a.reset,.panel a.gray.reset a.submit,.panel a.gray.reset button[type=reset],.panel a.gray.reset button[type=submit],.panel a.gray.submit .btn,.panel a.gray.submit .form a.reset,.panel a.gray.submit .form a.submit,.panel a.gray.submit .form button[type=reset],.panel a.gray.submit .form button[type=submit],.panel a.gray.submit a.reset,.panel a.gray.submit a.submit,.panel a.gray.submit button[type=reset],.panel a.gray.submit button[type=submit],.panel button.btn.gray a.reset,.panel button.btn.gray a.submit,.panel button.btn.gray button[type=reset],.panel button.btn.gray button[type=submit],.panel button.gray[type=reset] .btn,.panel button.gray[type=reset] .form a.reset,.panel button.gray[type=reset] .form a.submit,.panel button.gray[type=reset] .form button[type=reset],.panel button.gray[type=reset] .form button[type=submit],.panel button.gray[type=reset] a.reset,.panel button.gray[type=reset] a.submit,.panel button.gray[type=reset] button[type=reset],.panel button.gray[type=reset] button[type=submit],.panel button.gray[type=submit] .btn,.panel button.gray[type=submit] .form a.reset,.panel button.gray[type=submit] .form a.submit,.panel button.gray[type=submit] .form button[type=reset],.panel button.gray[type=submit] .form button[type=submit],.panel button.gray[type=submit] a.reset,.panel button.gray[type=submit] a.submit,.panel button.gray[type=submit] button[type=reset],.panel button.gray[type=submit] button[type=submit],.panel input.button.gray a.reset,.panel input.button.gray a.submit,.panel input.button.gray button[type=reset],.panel input.button.gray button[type=submit],a.btn.gray .btn,a.btn.gray .form a.reset,a.btn.gray .form a.submit,a.btn.gray .form button[type=reset],a.btn.gray .form button[type=submit],a.btn.gray .panel a.reset,a.btn.gray .panel a.submit,a.btn.gray .panel button[type=reset],a.btn.gray .panel button[type=submit],button.btn.gray .btn,button.btn.gray .form a.reset,button.btn.gray .form a.submit,button.btn.gray .form button[type=reset],button.btn.gray .form button[type=submit],button.btn.gray .panel a.reset,button.btn.gray .panel a.submit,button.btn.gray .panel button[type=reset],button.btn.gray .panel button[type=submit],input.button.gray .btn,input.button.gray .form a.reset,input.button.gray .form a.submit,input.button.gray .form button[type=reset],input.button.gray .form button[type=submit],input.button.gray .panel a.reset,input.button.gray .panel a.submit,input.button.gray .panel button[type=reset],input.button.gray .panel button[type=submit]{background:#59c1d5}.form a.dark.reset,.form a.dark.submit,.form button.dark[type=reset],.form button.dark[type=submit],.panel a.dark.reset,.panel a.dark.submit,.panel button.dark[type=reset],.panel button.dark[type=submit],a.btn.dark,button.btn.dark,input.button.dark{background-color:#2e2d2c!important;color:#ededed}header{background:#141414;margin:0;padding:5px}header img{vertical-align:middle;height:25px;margin-top:-5px}header ul{list-style-type:none;display:inline-block}header ul li{color:#ddd;display:inline-block;margin-left:20px}header ul li a{text-decoration:none;color:#ddd}header ul li a:hover{text-shadow:0 0 2px hsla(0,0%,100%,.1),0 2px 3px hsla(0,0%,100%,.5),0 6px 6px hsla(0,0%,100%,.2);color:#59c1d5}header .box-shadow{box-shadow:0 .25rem .75rem rgba(0,0,0,.05)}header .left-menu{flex-grow:1;align-self:center;padding:5px;color:#999}header .left-menu a{padding-left:5px;padding-right:5px;color:#fff;text-decoration:none;font-size:14px}header .right-menu{align-self:center}header .submenu{background:#2e2d2c;padding-top:10px;padding-left:60px}header .submenu .groups{margin-top:10px;margin-bottom:10px}header .submenu .groups a{background-color:#1e6977;padding:5px;border-top-left-radius:5px;border-top-right-radius:5px}header .submenu a{color:#ddd;text-decoration:none}header .submenu .application-list{display:flex;flex-direction:column}header .submenu .application-list div{padding:5px}.svg.blue{filter:invert(77%) sepia(17%) saturate(1201%) hue-rotate(144deg) brightness(89%) contrast(87%)}.svg.red{filter:invert(57%) sepia(89%) saturate(365%) hue-rotate(326deg) brightness(98%) contrast(92%)}span.muted{color:#706f6f}span.small{font-size:.9em}a{color:#f18c65;text-decoration:none}p{margin-top:5px;margin-bottom:15px}.text-shadow-1{text-shadow:2px 4px 3px rgba(0,0,0,.3)}h2,h3,h4{font-weight:600;margin-top:10px;margin-bottom:10px}.text-center{text-align:center}.text-right{text-align:right}.text-left{text-align:left}.text-blue{color:#59c1d5}.text-dark{color:#393938}.text-light{color:#ededed}.text-red{color:#f18c65}.text-white{color:#fff}.text-muted{color:#bbb}.table{table-layout:fixed;display:table}.table thead tr{background-color:#fff}.table thead tr.dark{color:#ededed}.table thead tr th{text-align:left;padding-top:5px;padding-bottom:5px}.table.striped tbody tr:nth-child(2n){background-color:#bfe7ef}.table.dark{color:#ededed}.table tbody tr td{padding-top:5px;padding-bottom:5px}body{background-color:#59c1d5}.starter{margin:10px;padding:0}.starter>.panel,.starter>.panels{margin:-10px!important;padding:0}.p-10{padding:10px}.fb-300px{flex-basis:400px}.flex-row{display:flex;flex-direction:row}.flex-grow-0{flex-grow:0}.panels{display:flex;flex-wrap:wrap;flex-direction:row}.panels .panel{margin:10px;padding:0;flex:1 0 auto;flex-basis:30%;max-width:50%}.panel{color:#141414;flex:1;margin:10px;padding:0;border-radius:3px;flex-direction:column;flex-basis:50%;min-width:400px;display:flex}.panel>*{display:block;box-sizing:border-box}.panel.full{flex:1 1 100%}.panel>h1,.panel>h2,.panel>h3{color:#fff;text-shadow:2px 4px 3px rgba(0,0,0,.3)}.panel.fill,.panel .fill{background-color:#f9f9f9;box-shadow:0 10px 36px 0 rgba(0,0,0,.16),0 0 0 1px rgba(0,0,0,.06);border-radius:3px;flex:1;padding:15px;display:block}.panel.fill h3,.panel .fill h3{color:#393938}.panel>.table{background-color:#f9f9f9;padding:15px}.panel .panel-header{font-size:24px;padding:10px;width:100%}.panel .panel-body{flex-grow:1}.panel .panel-body,.panel .panel-footer{padding:10px;width:100%}.col .panel{margin-left:0;margin-right:0}.f-grow{flex-grow:1}.facts th{font-weight:500;text-align:right;min-width:150px}.dropdown{position:relative;display:inline-block}.dropdown .content{display:none;position:absolute;z-index:2;background-color:#fff;padding:10px;border:1px solid #333;box-shadow:0 54px 55px rgba(0,0,0,.25),0 -12px 30px rgba(0,0,0,.12),0 4px 6px rgba(0,0,0,.12),0 12px 13px rgba(0,0,0,.17),0 -3px 5px rgba(0,0,0,.09)}.dropdown .content.show{display:block}.pointer{cursor:pointer}.menu{visibility:hidden;position:fixed;background-color:teal;word-wrap:unset;margin:0;transition:.5s;opacity:0;padding:10px;border:1px dashed red}.menu.shown{opacity:1;visibility:visible}h2.active{border-bottom:3px solid orange;color:#fff}.dimScreen{display:none;position:fixed;padding:0;margin:0;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,.5);z-index:10}#note{display:none;background:#d8eef4;color:#000;position:absolute;z-index:11;padding:10px;min-width:250px;box-shadow:0 8px 24px hsla(0,0%,100%,.4);border-radius:5px;border-image-slice:1;border:5px solid;border-image-source:linear-gradient(270deg,#59c1d5,#abdbe7)}#note>div{margin-bottom:20px}#note button{cursor:pointer;font-size:.8em;float:right;background:#59c1d5;border-radius:2px;border-image-slice:1;border:2px solid;border-image-source:linear-gradient(270deg,#59c1d5,#abdbe7)}#note h3{margin-top:5px}.guide-highlight{position:relative;z-index:11;box-shadow:0 8px 24px hsla(0,0%,100%,.4);border-radius:2px;padding:4px}.guide-highlight,.guide-highlight i{background-color:#fff!important;color:#59c1d5!important}.toast-center-center{top:50%;left:50%;transform:translate(-50%,-50%)}.toast-top-center{top:0;right:0;width:100%}.toast-bottom-center{bottom:0;right:0;width:100%}.toast-top-full-width{top:0;right:0;width:100%}.toast-bottom-full-width{bottom:0;right:0;width:100%}.toast-top-left{top:12px;left:12px}.toast-top-right{top:12px;right:12px}.toast-bottom-right{right:12px;bottom:12px}.toast-bottom-left{bottom:12px;left:12px}.toast-title{font-weight:700}.toast-message{word-wrap:break-word}.toast-message a,.toast-message label{color:#fff}.toast-message a:hover{color:#ccc;text-decoration:none}.toast-close-button{position:relative;right:-.3em;top:-.3em;float:right;font-size:20px;font-weight:700;color:#fff;text-shadow:0 1px 0 #fff}.toast-close-button:focus,.toast-close-button:hover{color:#000;text-decoration:none;cursor:pointer;opacity:.4}button.toast-close-button{padding:0;cursor:pointer;background:transparent;border:0}.toast-container{pointer-events:none;position:fixed;z-index:999999}.toast-container *{box-sizing:border-box}.toast-container .ngx-toastr{position:relative;overflow:hidden;margin:0 0 6px;padding:15px 15px 15px 50px;width:300px;border-radius:3px 3px 3px 3px;background-position:15px;background-repeat:no-repeat;background-size:24px;box-shadow:0 0 12px #999;color:#fff}.toast-container .ngx-toastr:hover{box-shadow:0 0 12px #000;opacity:1;cursor:pointer}.toast-info{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOS4wNDMgOCA4IDExOS4wODMgOCAyNTZjMCAxMzYuOTk3IDExMS4wNDMgMjQ4IDI0OCAyNDhzMjQ4LTExMS4wMDMgMjQ4LTI0OEM1MDQgMTE5LjA4MyAzOTIuOTU3IDggMjU2IDh6bTAgMTEwYzIzLjE5NiAwIDQyIDE4LjgwNCA0MiA0MnMtMTguODA0IDQyLTQyIDQyLTQyLTE4LjgwNC00Mi00MiAxOC44MDQtNDIgNDItNDJ6bTU2IDI1NGMwIDYuNjI3LTUuMzczIDEyLTEyIDEyaC04OGMtNi42MjcgMC0xMi01LjM3My0xMi0xMnYtMjRjMC02LjYyNyA1LjM3My0xMiAxMi0xMmgxMnYtNjRoLTEyYy02LjYyNyAwLTEyLTUuMzczLTEyLTEydi0yNGMwLTYuNjI3IDUuMzczLTEyIDEyLTEyaDY0YzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MTAwaDEyYzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MjR6Jy8+PC9zdmc+")}.toast-error{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOSA4IDggMTE5IDggMjU2czExMSAyNDggMjQ4IDI0OCAyNDgtMTExIDI0OC0yNDhTMzkzIDggMjU2IDh6bTEyMS42IDMxMy4xYzQuNyA0LjcgNC43IDEyLjMgMCAxN0wzMzggMzc3LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwyNTYgMzEybC02NS4xIDY1LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwxMzQuNCAzMzhjLTQuNy00LjctNC43LTEyLjMgMC0xN2w2NS42LTY1LTY1LjYtNjUuMWMtNC43LTQuNy00LjctMTIuMyAwLTE3bDM5LjYtMzkuNmM0LjctNC43IDEyLjMtNC43IDE3IDBsNjUgNjUuNyA2NS4xLTY1LjZjNC43LTQuNyAxMi4zLTQuNyAxNyAwbDM5LjYgMzkuNmM0LjcgNC43IDQuNyAxMi4zIDAgMTdMMzEyIDI1Nmw2NS42IDY1LjF6Jy8+PC9zdmc+")}.toast-success{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTE3My44OTggNDM5LjQwNGwtMTY2LjQtMTY2LjRjLTkuOTk3LTkuOTk3LTkuOTk3LTI2LjIwNiAwLTM2LjIwNGwzNi4yMDMtMzYuMjA0YzkuOTk3LTkuOTk4IDI2LjIwNy05Ljk5OCAzNi4yMDQgMEwxOTIgMzEyLjY5IDQzMi4wOTUgNzIuNTk2YzkuOTk3LTkuOTk3IDI2LjIwNy05Ljk5NyAzNi4yMDQgMGwzNi4yMDMgMzYuMjA0YzkuOTk3IDkuOTk3IDkuOTk3IDI2LjIwNiAwIDM2LjIwNGwtMjk0LjQgMjk0LjQwMWMtOS45OTggOS45OTctMjYuMjA3IDkuOTk3LTM2LjIwNC0uMDAxeicvPjwvc3ZnPg==")}.toast-warning{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1NzYgNTEyJyB3aWR0aD0nNTc2JyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTU2OS41MTcgNDQwLjAxM0M1ODcuOTc1IDQ3Mi4wMDcgNTY0LjgwNiA1MTIgNTI3Ljk0IDUxMkg0OC4wNTRjLTM2LjkzNyAwLTU5Ljk5OS00MC4wNTUtNDEuNTc3LTcxLjk4N0wyNDYuNDIzIDIzLjk4NWMxOC40NjctMzIuMDA5IDY0LjcyLTMxLjk1MSA4My4xNTQgMGwyMzkuOTQgNDE2LjAyOHpNMjg4IDM1NGMtMjUuNDA1IDAtNDYgMjAuNTk1LTQ2IDQ2czIwLjU5NSA0NiA0NiA0NiA0Ni0yMC41OTUgNDYtNDYtMjAuNTk1LTQ2LTQ2LTQ2em0tNDMuNjczLTE2NS4zNDZsNy40MTggMTM2Yy4zNDcgNi4zNjQgNS42MDkgMTEuMzQ2IDExLjk4MiAxMS4zNDZoNDguNTQ2YzYuMzczIDAgMTEuNjM1LTQuOTgyIDExLjk4Mi0xMS4zNDZsNy40MTgtMTM2Yy4zNzUtNi44NzQtNS4wOTgtMTIuNjU0LTExLjk4Mi0xMi42NTRoLTYzLjM4M2MtNi44ODQgMC0xMi4zNTYgNS43OC0xMS45ODEgMTIuNjU0eicvPjwvc3ZnPg==")}.toast-container.toast-bottom-center .ngx-toastr,.toast-container.toast-top-center .ngx-toastr{width:300px;margin-left:auto;margin-right:auto}.toast-container.toast-bottom-full-width .ngx-toastr,.toast-container.toast-top-full-width .ngx-toastr{width:96%;margin-left:auto;margin-right:auto}.ngx-toastr{background-color:#030303;pointer-events:auto}.toast-success{background-color:#51a351}.toast-error{background-color:#bd362f}.toast-info{background-color:#2f96b4}.toast-warning{background-color:#f89406}.toast-progress{position:absolute;left:0;bottom:0;height:4px;background-color:#000;opacity:.4}@media all and (max-width: 240px){.toast-container .ngx-toastr.div{padding:8px 8px 8px 50px;width:11em}.toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width: 241px) and (max-width: 480px){.toast-container .ngx-toastr.div{padding:8px 8px 8px 50px;width:18em}.toast-container .toast-close-button{right:-.2em;top:-.2em}}@media all and (min-width: 481px) and (max-width: 768px){.toast-container .ngx-toastr.div{padding:15px 15px 15px 50px;width:25em}} \ No newline at end of file diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/styles.css b/src/Server/Coderr.Server.WebSite/wwwroot/styles.css new file mode 100644 index 00000000..fad814ba --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/wwwroot/styles.css @@ -0,0 +1,1443 @@ +/*!**************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[5].rules[0].oneOf[1].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[5].rules[0].oneOf[1].use[2]!./node_modules/resolve-url-loader/index.js??ruleSet[1].rules[5].rules[1].use[0]!./node_modules/sass-loader/dist/cjs.js??ruleSet[1].rules[5].rules[1].use[1]!./src/styles/site.scss ***! + \**************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/ +html { + box-sizing: border-box; + font-size: 15px; + font-family: "Sofia Pro", sans-serif; + height: 100%; +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body, h1, h2, h3, h4, h5, h6, p, ol, ul { + margin: 0; + padding: 0; + font-weight: normal; +} + +ol, ul { + list-style: none; +} + +img { + max-width: 100%; + height: auto; +} + +.card input[type=radio] { + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + outline: 0; + background: #eee; + height: 16px; + width: 16px; + border: 1px solid white; +} + +.card input[type=radio]:checked { + background: #59c1d5; + color: #59c1d5 !important; +} + +.card input[type=radio]:hover { + filter: brightness(90%); +} + +.card input[type=radio]:disabled { + background: #393938; + opacity: 0.6; + pointer-events: none; +} + +.card input[type=radio]:after { + content: ""; + position: relative; + left: 40%; + top: 20%; + width: 15%; + height: 40%; + display: none; +} + +.card input[type=radio]:checked:after { + display: block; +} + +.card input[type=radio]:disabled:after { + border-color: #f4f4f4; +} + +.bg.white { + background-color: white; +} + +.alert { + padding: 10px; +} + +.alert.warning { + background-color: #f18c65; + border: 1px solid #ed6936; + color: #ededed; +} + +.alert.success { + background-color: #d8eef4; + border: 1px solid #b0dde9; +} + +.card input[type=checkbox] { + cursor: pointer; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + outline: 0; + background: #eee; + height: 16px; + width: 16px; + border: 1px solid white; +} + +.card input[type=checkbox]:checked { + background: #59c1d5; + color: #59c1d5 !important; +} + +.card input[type=checkbox]:hover { + filter: brightness(90%); +} + +.card input[type=checkbox]:disabled { + background: #393938; + opacity: 0.6; + pointer-events: none; +} + +.card input[type=checkbox]:after { + content: ""; + position: relative; + left: 40%; + top: 20%; + width: 15%; + height: 40%; + display: none; +} + +.card input[type=checkbox]:checked:after { + display: block; +} + +.card input[type=checkbox]:disabled:after { + border-color: #f4f4f4; +} + +input:invalid { + box-shadow: 0 0 5px 1px #f18c65; +} + +input:focus:invalid { + box-shadow: none; +} + +input, select, textarea { + padding: 4px; + border: 1px solid #404040; + border-radius: 3px; +} + +.form .form-group { + margin-top: 15px; +} + +.form label { + display: block; + font-weight: bold; +} + +.form label.inline { + display: inline; +} + +.form input.inline { + display: inline; +} + +.form input[type=text], +.form input[type=number], +.form input[type=email], +.form input[type=password], +.form textarea, +.form select { + background-color: #abdbe7; + border: 1px solid #216272; + border-radius: 3px; + padding: 8px; + border: #5cb9d0; + width: 100%; +} + +.form textarea { + min-height: 100px; + width: 100%; +} + +.form button { + display: inline-flex; +} + +.form button[type=submit], .form a.submit { + background-color: #59c1d5; + border: #288ca0; + color: #ededed; + width: inherit; +} + +.form button[type=reset], .form a.reset { + color: #ededed; + background-color: #33b0c8; + border: #dc4c14; +} + +.row { + display: flex; + width: 100%; + flex-flow: row wrap; + max-width: 100%; +} + +.container { + display: block; + max-width: 1400px; + margin: 0 auto; + padding: 30px; + padding: -10px; +} + +.container h1 { + font-size: 48px; +} + +.col { + justify-content: center; + align-items: start; + flex-basis: 0; + /*flex-direction: column; + flex-basis: auto;*/ + flex: 1 1 200px; + margin: 10px; + /* pre > code wont wrap otherwise */ + min-width: 0; +} + +.col-2 { + flex: 2; +} + +.col-3 { + flex: 3; +} + +.hidden { + display: none; + opacity: 0; +} + +.w-100 { + width: 100%; +} + +@media only screen and (max-width: 600px) { + .col { + display: block; + width: 100%; + } +} + +.y-center { + align-items: center; +} + +.x-center { + justify-content: center; +} + +.mx-auto { + margin: 0 auto; +} + +.mt-0 { + margin-top: 0px; +} + +.mb-0 { + margin-bottom: 0px; +} + +.ml-0 { + margin-left: 0px !important; +} + +.m-0 { + margin: 0px; +} + +.pt-0 { + padding-top: 0px; +} + +.pl-0 { + padding-left: 0px; +} + +.pr-0 { + padding-right: 0px; +} + +.pb-0 { + padding-bottom: 0px; +} + +.p-0 { + padding: 0px; +} + +.mt-1 { + margin-top: 5px; +} + +.mb-1 { + margin-bottom: 5px; +} + +.ml-1 { + margin-left: 5px !important; +} + +.m-1 { + margin: 5px; +} + +.pt-1 { + padding-top: 5px; +} + +.pl-1 { + padding-left: 5px; +} + +.pr-1 { + padding-right: 5px; +} + +.pb-1 { + padding-bottom: 5px; +} + +.p-1 { + padding: 5px; +} + +.mt-2 { + margin-top: 10px; +} + +.mb-2 { + margin-bottom: 10px; +} + +.ml-2 { + margin-left: 10px !important; +} + +.m-2 { + margin: 10px; +} + +.pt-2 { + padding-top: 10px; +} + +.pl-2 { + padding-left: 10px; +} + +.pr-2 { + padding-right: 10px; +} + +.pb-2 { + padding-bottom: 10px; +} + +.p-2 { + padding: 10px; +} + +.mt-3 { + margin-top: 15px; +} + +.mb-3 { + margin-bottom: 15px; +} + +.ml-3 { + margin-left: 15px !important; +} + +.m-3 { + margin: 15px; +} + +.pt-3 { + padding-top: 15px; +} + +.pl-3 { + padding-left: 15px; +} + +.pr-3 { + padding-right: 15px; +} + +.pb-3 { + padding-bottom: 15px; +} + +.p-3 { + padding: 15px; +} + +.mt-4 { + margin-top: 20px; +} + +.mb-4 { + margin-bottom: 20px; +} + +.ml-4 { + margin-left: 20px !important; +} + +.m-4 { + margin: 20px; +} + +.pt-4 { + padding-top: 20px; +} + +.pl-4 { + padding-left: 20px; +} + +.pr-4 { + padding-right: 20px; +} + +.pb-4 { + padding-bottom: 20px; +} + +.p-4 { + padding: 20px; +} + +.mt-5 { + margin-top: 25px; +} + +.mb-5 { + margin-bottom: 25px; +} + +.ml-5 { + margin-left: 25px !important; +} + +.m-5 { + margin: 25px; +} + +.pt-5 { + padding-top: 25px; +} + +.pl-5 { + padding-left: 25px; +} + +.pr-5 { + padding-right: 25px; +} + +.pb-5 { + padding-bottom: 25px; +} + +.p-5 { + padding: 25px; +} + +.panel .btn, .panel button[type=reset], .panel a.reset, .panel button[type=submit], .panel a.submit { + background-color: #ededed; + color: #393938; + display: inline-flex; +} + +.panel.fill .btn, .panel.fill button[type=reset], .panel.fill a.reset, .panel.fill button[type=submit], .panel.fill a.submit, .panel .fill .btn, .panel .fill button[type=reset], .panel .fill a.reset, .panel .fill button[type=submit], .panel .fill a.submit { + background-color: #59c1d5; + border-color: #59c1d5; + color: #ededed; + display: inline-flex; +} + +.panel button[type=submit], .panel a.submit { + background-color: #ededed; + border-color: #ededed; + border: #d4d4d4; + color: #393938; + width: inherit; +} + +.panel button[type=reset], .panel a.reset { + color: #ededed; + background-color: #f18c65; + border-color: #f18c65; + border: #ed6936; +} + +a.btn, .form a.submit, .form a.reset, .panel a.submit, .panel a.reset, +button.btn, +.form button[type=submit], +.form button[type=reset], +.panel button[type=submit], +.panel button[type=reset], +input.button { + padding: 10px 12px; + margin-top: 15px; + margin-right: 5px; + border-radius: 3px; + text-decoration: none; + box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1); + text-shadow: 1px 1px rgba(0, 0, 0, 0.05); + text-align: center; + cursor: pointer; + display: inline-block; +} + +a.btn.small, .form a.small.submit, .form a.small.reset, .panel a.small.submit, .panel a.small.reset, +button.btn.small, +.form button.small[type=submit], +.form button.small[type=reset], +.panel button.small[type=submit], +.panel button.small[type=reset], +input.button.small { + margin-top: 5px; + margin-right: 2px; + padding: 4px 6px; + font-size: 0.9em; +} + +a.btn.default, .form a.default.submit, .form a.default.reset, .panel a.default.submit, .panel a.default.reset, +button.btn.default, +.form button.default[type=submit], +.form button.default[type=reset], +.panel button.default[type=submit], +.panel button.default[type=reset], +input.button.default { + background: white; + color: #393938; +} + +a.btn.block, .form a.block.submit, .form a.block.reset, .panel a.block.submit, .panel a.block.reset, +button.btn.block, +.form button.block[type=submit], +.form button.block[type=reset], +.panel button.block[type=submit], +.panel button.block[type=reset], +input.button.block { + display: block; + width: 100%; +} + +a.btn.light, .form a.light.submit, .form a.light.reset, .panel a.light.submit, .panel a.light.reset, +button.btn.light, +.form button.light[type=submit], +.form button.light[type=reset], +.panel button.light[type=submit], +.panel button.light[type=reset], +input.button.light { + background-color: #ededed; + color: #393938; +} + +a.btn.dark, .form a.dark.submit, .form a.dark.reset, .panel a.dark.submit, .panel a.dark.reset, +button.btn.dark, +.form button.dark[type=submit], +.form button.dark[type=reset], +.panel button.dark[type=submit], +.panel button.dark[type=reset], +input.button.dark { + background-color: #393938; + color: #ededed; +} + +a.btn.red, .form a.red.submit, .form a.red.reset, .panel a.red.submit, .panel a.red.reset, +button.btn.red, +.form button.red[type=submit], +.form button.red[type=reset], +.panel button.red[type=submit], +.panel button.red[type=reset], +input.button.red { + background-color: #f18c65 !important; + color: white; + border: 1px solid #f18960; + transition: 0.5s; +} + +a.btn.red:hover, .form a.red.submit:hover, .form a.red.reset:hover, .panel a.red.submit:hover, .panel a.red.reset:hover, +button.btn.red:hover, +.form button.red[type=submit]:hover, +.form button.red[type=reset]:hover, +.panel button.red[type=submit]:hover, +.panel button.red[type=reset]:hover, +input.button.red:hover { + background-color: #eb581f !important; +} + +a.btn.blue, .form a.blue.submit, .form a.blue.reset, .panel a.blue.submit, .panel a.blue.reset, a.btn.default, .form a.default.submit, .form a.default.reset, .panel a.default.submit, .panel a.default.reset, +button.btn.blue, +.form button.blue[type=submit], +.form button.blue[type=reset], +.panel button.blue[type=submit], +.panel button.blue[type=reset], +button.btn.default, +.form button.default[type=submit], +.form button.default[type=reset], +.panel button.default[type=submit], +.panel button.default[type=reset], +input.button.blue, +input.button.default { + background-color: #59c1d5 !important; + color: white; + border: 1px solid #55bfd4; +} + +a.btn.red50, .form a.red50.submit, .form a.red50.reset, .panel a.red50.submit, .panel a.red50.reset, +button.btn.red50, +.form button.red50[type=submit], +.form button.red50[type=reset], +.panel button.red50[type=submit], +.panel button.red50[type=reset], +input.button.red50 { + background-color: rgba(241, 140, 101, 0.75) !important; + color: white; + border: 1px solid rgba(241, 140, 101, 0.95); +} + +a.btn.red-2, .form a.red-2.submit, .form a.red-2.reset, .panel a.red-2.submit, .panel a.red-2.reset, +button.btn.red-2, +.form button.red-2[type=submit], +.form button.red-2[type=reset], +.panel button.red-2[type=submit], +.panel button.red-2[type=reset], +input.button.red-2 { + background-color: #f39d7c !important; + color: white; + border: 1px solid rgba(241, 140, 101, 0.5); +} + +a.btn.white, .form a.white.submit, .form a.white.reset, .panel a.white.submit, .panel a.white.reset, +button.btn.white, +.form button.white[type=submit], +.form button.white[type=reset], +.panel button.white[type=submit], +.panel button.white[type=reset], +input.button.white { + background-color: white !important; + color: #2e2d2c; + border: 1px solid #fcfcfc; +} + +a.btn.gray, .form a.gray.submit, .form a.gray.reset, .panel a.gray.submit, .panel a.gray.reset, +button.btn.gray, +.form button.gray[type=submit], +.form button.gray[type=reset], +.panel button.gray[type=submit], +.panel button.gray[type=reset], +input.button.gray { + background-color: #eee !important; + color: #141414; + border: 1px solid #fcfcfc; +} + +a.btn.gray .btn, .form a.gray.submit .btn, .form a.gray.reset .btn, .panel a.gray.submit .btn, .panel a.gray.reset .btn, a.btn.gray .form button[type=submit], .form a.btn.gray button[type=submit], .form a.gray.submit button[type=submit], .form a.gray.reset button[type=submit], .panel a.gray.submit .form button[type=submit], .form .panel a.gray.submit button[type=submit], .panel a.gray.reset .form button[type=submit], .form .panel a.gray.reset button[type=submit], a.btn.gray .form a.submit, .form a.btn.gray a.submit, .form a.gray.submit a.submit, .form a.gray.reset a.submit, .panel a.gray.submit .form a.submit, .form .panel a.gray.submit a.submit, .panel a.gray.reset .form a.submit, .form .panel a.gray.reset a.submit, a.btn.gray .form button[type=reset], .form a.btn.gray button[type=reset], .form a.gray.submit button[type=reset], .form a.gray.reset button[type=reset], .panel a.gray.submit .form button[type=reset], .form .panel a.gray.submit button[type=reset], .panel a.gray.reset .form button[type=reset], .form .panel a.gray.reset button[type=reset], a.btn.gray .form a.reset, .form a.btn.gray a.reset, .form a.gray.submit a.reset, .form a.gray.reset a.reset, .panel a.gray.submit .form a.reset, .form .panel a.gray.submit a.reset, .panel a.gray.reset .form a.reset, .form .panel a.gray.reset a.reset, a.btn.gray .panel button[type=submit], .panel a.btn.gray button[type=submit], .form a.gray.submit .panel button[type=submit], .panel .form a.gray.submit button[type=submit], .form a.gray.reset .panel button[type=submit], .panel .form a.gray.reset button[type=submit], .panel a.gray.submit button[type=submit], .panel a.gray.reset button[type=submit], a.btn.gray .panel a.submit, .panel a.btn.gray a.submit, .form a.gray.submit .panel a.submit, .panel .form a.gray.submit a.submit, .form a.gray.reset .panel a.submit, .panel .form a.gray.reset a.submit, .panel a.gray.submit a.submit, .panel a.gray.reset a.submit, a.btn.gray .panel button[type=reset], .panel a.btn.gray button[type=reset], .form a.gray.submit .panel button[type=reset], .panel .form a.gray.submit button[type=reset], .form a.gray.reset .panel button[type=reset], .panel .form a.gray.reset button[type=reset], .panel a.gray.submit button[type=reset], .panel a.gray.reset button[type=reset], a.btn.gray .panel a.reset, .panel a.btn.gray a.reset, .form a.gray.submit .panel a.reset, .panel .form a.gray.submit a.reset, .form a.gray.reset .panel a.reset, .panel .form a.gray.reset a.reset, .panel a.gray.submit a.reset, .panel a.gray.reset a.reset, +button.btn.gray .btn, +.form button.gray[type=submit] .btn, +.form button.gray[type=reset] .btn, +.panel button.gray[type=submit] .btn, +.panel button.gray[type=reset] .btn, +button.btn.gray .form button[type=submit], +.form button.btn.gray button[type=submit], +.form button.gray[type=submit] button[type=submit], +.form button.gray[type=reset] button[type=submit], +.panel button.gray[type=submit] .form button[type=submit], +.form .panel button.gray[type=submit] button[type=submit], +.panel button.gray[type=reset] .form button[type=submit], +.form .panel button.gray[type=reset] button[type=submit], +button.btn.gray .form a.submit, +.form button.btn.gray a.submit, +.form button.gray[type=submit] a.submit, +.form button.gray[type=reset] a.submit, +.panel button.gray[type=submit] .form a.submit, +.form .panel button.gray[type=submit] a.submit, +.panel button.gray[type=reset] .form a.submit, +.form .panel button.gray[type=reset] a.submit, +button.btn.gray .form button[type=reset], +.form button.btn.gray button[type=reset], +.form button.gray[type=submit] button[type=reset], +.form button.gray[type=reset] button[type=reset], +.panel button.gray[type=submit] .form button[type=reset], +.form .panel button.gray[type=submit] button[type=reset], +.panel button.gray[type=reset] .form button[type=reset], +.form .panel button.gray[type=reset] button[type=reset], +button.btn.gray .form a.reset, +.form button.btn.gray a.reset, +.form button.gray[type=submit] a.reset, +.form button.gray[type=reset] a.reset, +.panel button.gray[type=submit] .form a.reset, +.form .panel button.gray[type=submit] a.reset, +.panel button.gray[type=reset] .form a.reset, +.form .panel button.gray[type=reset] a.reset, +button.btn.gray .panel button[type=submit], +.panel button.btn.gray button[type=submit], +.form button.gray[type=submit] .panel button[type=submit], +.panel .form button.gray[type=submit] button[type=submit], +.form button.gray[type=reset] .panel button[type=submit], +.panel .form button.gray[type=reset] button[type=submit], +.panel button.gray[type=submit] button[type=submit], +.panel button.gray[type=reset] button[type=submit], +button.btn.gray .panel a.submit, +.panel button.btn.gray a.submit, +.form button.gray[type=submit] .panel a.submit, +.panel .form button.gray[type=submit] a.submit, +.form button.gray[type=reset] .panel a.submit, +.panel .form button.gray[type=reset] a.submit, +.panel button.gray[type=submit] a.submit, +.panel button.gray[type=reset] a.submit, +button.btn.gray .panel button[type=reset], +.panel button.btn.gray button[type=reset], +.form button.gray[type=submit] .panel button[type=reset], +.panel .form button.gray[type=submit] button[type=reset], +.form button.gray[type=reset] .panel button[type=reset], +.panel .form button.gray[type=reset] button[type=reset], +.panel button.gray[type=submit] button[type=reset], +.panel button.gray[type=reset] button[type=reset], +button.btn.gray .panel a.reset, +.panel button.btn.gray a.reset, +.form button.gray[type=submit] .panel a.reset, +.panel .form button.gray[type=submit] a.reset, +.form button.gray[type=reset] .panel a.reset, +.panel .form button.gray[type=reset] a.reset, +.panel button.gray[type=submit] a.reset, +.panel button.gray[type=reset] a.reset, +input.button.gray .btn, +input.button.gray .form button[type=submit], +.form input.button.gray button[type=submit], +input.button.gray .form a.submit, +.form input.button.gray a.submit, +input.button.gray .form button[type=reset], +.form input.button.gray button[type=reset], +input.button.gray .form a.reset, +.form input.button.gray a.reset, +input.button.gray .panel button[type=submit], +.panel input.button.gray button[type=submit], +input.button.gray .panel a.submit, +.panel input.button.gray a.submit, +input.button.gray .panel button[type=reset], +.panel input.button.gray button[type=reset], +input.button.gray .panel a.reset, +.panel input.button.gray a.reset { + background: #59c1d5; +} + +a.btn.dark, .form a.dark.submit, .form a.dark.reset, .panel a.dark.submit, .panel a.dark.reset, +button.btn.dark, +.form button.dark[type=submit], +.form button.dark[type=reset], +.panel button.dark[type=submit], +.panel button.dark[type=reset], +input.button.dark { + background-color: #2e2d2c !important; + color: #ededed; +} + +header { + background: #141414; + margin: 0; + padding: 5px; + /*.main { + box-shadow: 0 15px 5px lighten($nav-bg, 20%); + } + */ +} + +header img { + vertical-align: middle; + height: 25px; + margin-top: -5px; +} + +header ul { + list-style-type: none; + display: inline-block; +} + +header ul li { + color: #dddddd; + display: inline-block; + margin-left: 20px; +} + +header ul li a { + text-decoration: none; + color: #dddddd; +} + +header ul li a:hover { + text-shadow: 0px 0px 2px rgba(255, 255, 255, 0.1), 0px 2px 3px rgba(255, 255, 255, 0.5), 0px 6px 6px rgba(255, 255, 255, 0.2); + color: #59c1d5; +} + +header .box-shadow { + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.05); +} + +header .left-menu { + flex-grow: 1; + align-self: center; + padding: 5px; + color: #999; +} + +header .left-menu a { + padding-left: 5px; + padding-right: 5px; + color: white; + text-decoration: none; + font-size: 14px; +} + +header .right-menu { + align-self: center; +} + +header .submenu { + background: #2e2d2c; + padding-top: 10px; + padding-left: 60px; +} + +header .submenu .groups { + margin-top: 10px; + margin-bottom: 10px; +} + +header .submenu .groups a { + background-color: #1e6977; + padding: 5px; + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} + +header .submenu a { + color: #dddddd; + text-decoration: none; +} + +header .submenu .application-list { + display: flex; + flex-direction: column; +} + +header .submenu .application-list div { + padding: 5px; +} + +.svg.blue { + filter: invert(77%) sepia(17%) saturate(1201%) hue-rotate(144deg) brightness(89%) contrast(87%); +} + +.svg.red { + filter: invert(57%) sepia(89%) saturate(365%) hue-rotate(326deg) brightness(98%) contrast(92%); +} + +span.muted { + color: #706f6f; +} + +span.small { + font-size: 0.9em; +} + +a { + color: #f18c65; + text-decoration: none; +} + +p { + margin-top: 5px; + margin-bottom: 15px; +} + +.text-shadow-1 { + text-shadow: 2px 4px 3px rgba(0, 0, 0, 0.3); +} + +h2, h3, h4 { + font-weight: 600; + margin-top: 10px; + margin-bottom: 10px; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-left { + text-align: left; +} + +.text-blue { + color: #59c1d5; +} + +.text-dark { + color: #393938; +} + +.text-light { + color: #ededed; +} + +.text-red { + color: #f18c65; +} + +.text-white { + color: #fff; +} + +.text-muted { + color: #bbb; +} + +.table { + table-layout: fixed; + display: table; +} + +.table thead tr { + background-color: #fff; +} + +.table thead tr th { + text-align: left; + padding-top: 5px; + padding-bottom: 5px; +} + +.table.striped tbody tr:nth-child(even) { + background-color: #bfe7ef; +} + +.table tbody tr td { + padding-top: 5px; + padding-bottom: 5px; +} + +body { + background-color: #59c1d5; +} + +.main-view { + /**padding: 10px; Let all views control this **/ +} + +.starter { + margin: 10px; + padding: 0; +} + +.starter > .panel, .starter > .panels { + margin: -10px !important; + padding: 0; +} + +.p-10 { + padding: 10px; +} + +.fb-300px { + flex-basis: 400px; +} + +.flex-row { + display: flex; + flex-direction: row; +} + +.flex-grow-0 { + flex-grow: 0; +} + +.panels { + display: flex; + flex-wrap: wrap; + flex-direction: row; +} + +.panels .panel { + margin: 10px; + padding: 0; + flex: 1 0 auto; + flex-basis: 30%; + max-width: 50%; +} + +.panel { + color: #141414; + flex: 1; + margin: 10px; + padding: 0; + border-radius: 3px; + flex-direction: column; + flex-basis: 50%; + min-width: 400px; + display: flex; +} + +.panel > * { + display: block; + box-sizing: border-box; +} + +.panel.full { + flex: 1 1 100%; +} + +.panel > h1, .panel > h2, .panel > h3 { + color: white; + text-shadow: 2px 4px 3px rgba(0, 0, 0, 0.3); +} + +.panel.fill, .panel .fill { + background-color: #f9f9f9; + box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px; + border-radius: 3px; + flex: 1; + padding: 15px; + display: block; +} + +.panel.fill h3, .panel .fill h3 { + color: #393938; +} + +.panel > .table { + background-color: #f9f9f9; + padding: 15px; +} + +.panel .panel-header { + font-size: 24px; + padding: 10px; + width: 100%; +} + +.panel .panel-body { + padding: 10px; + flex-grow: 1; + width: 100%; +} + +.panel .panel-footer { + padding: 10px; + width: 100%; +} + +.col .panel { + margin-left: 0; + margin-right: 0; +} + +.f-grow { + flex-grow: 1; +} + +.facts th { + font-weight: 500; + text-align: right; + min-width: 150px; +} + +.dropdown { + position: relative; + display: inline-block; +} + +.dropdown .content { + display: none; + position: absolute; + z-index: 2; + background-color: #fff; + padding: 10px; + border: 1px solid #333; + box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px; +} + +.dropdown .content.show { + display: block; +} + +.pointer { + cursor: pointer; +} + +.menu { + visibility: hidden; + position: fixed; + background-color: teal; + word-wrap: unset; + margin: 0; + transition: 0.5s; + opacity: 0; + padding: 10px; + border: red 1px dashed; +} + +.menu.shown { + opacity: 1; + visibility: visible; +} + +h2.active { + border-bottom: orange 3px solid; + color: white; +} + +/** Guide CSS */ + +.dimScreen { + display: none; + position: fixed; + padding: 0; + margin: 0; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + z-index: 10; +} + +#note { + display: none; + /*background-color: #FDFF47;*/ + background: #d8eef4; + color: #000; + position: absolute; + z-index: 11; + padding: 10px; + min-width: 250px; + box-shadow: rgba(255, 255, 255, 0.4) 0px 8px 24px; + border-radius: 5px; + border: 10px solid; + border-image-slice: 1; + border-width: 5px; + border-image-source: linear-gradient(to left, #59c1d5, #abdbe7); +} + +#note > div { + margin-bottom: 20px; +} + +#note button { + cursor: pointer; + font-size: 0.8em; + float: right; + background: #59c1d5; + border-radius: 2px; + border: 5px solid; + border-image-slice: 1; + border-width: 2px; + border-image-source: linear-gradient(to left, #59c1d5, #abdbe7); +} + +#note h3 { + margin-top: 5px; +} + +.guide-highlight { + position: relative; + background-color: #fff !important; + z-index: 11; + box-shadow: rgba(255, 255, 255, 0.4) 0px 8px 24px; + border-radius: 2px; + padding: 4px; + color: #59c1d5 !important; +} + +.guide-highlight i { + background-color: #fff !important; + color: #59c1d5 !important; +} +/*!****************************************************************************************************************************************************************************************************************************!*\ + !*** css ./node_modules/css-loader/dist/cjs.js??ruleSet[1].rules[4].rules[0].oneOf[1].use[1]!./node_modules/postcss-loader/dist/cjs.js??ruleSet[1].rules[4].rules[0].oneOf[1].use[2]!./node_modules/ngx-toastr/toastr.css ***! + \****************************************************************************************************************************************************************************************************************************/ +/* based on angular-toastr css https://github.com/Foxandxss/angular-toastr/blob/cb508fe6801d6b288d3afc525bb40fee1b101650/dist/angular-toastr.css */ + +/* position */ + +.toast-center-center { + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.toast-top-center { + top: 0; + right: 0; + width: 100%; +} + +.toast-bottom-center { + bottom: 0; + right: 0; + width: 100%; +} + +.toast-top-full-width { + top: 0; + right: 0; + width: 100%; +} + +.toast-bottom-full-width { + bottom: 0; + right: 0; + width: 100%; +} + +.toast-top-left { + top: 12px; + left: 12px; +} + +.toast-top-right { + top: 12px; + right: 12px; +} + +.toast-bottom-right { + right: 12px; + bottom: 12px; +} + +.toast-bottom-left { + bottom: 12px; + left: 12px; +} + +/* toast styles */ + +.toast-title { + font-weight: bold; +} + +.toast-message { + word-wrap: break-word; +} + +.toast-message a, +.toast-message label { + color: #FFFFFF; +} + +.toast-message a:hover { + color: #CCCCCC; + text-decoration: none; +} + +.toast-close-button { + position: relative; + right: -0.3em; + top: -0.3em; + float: right; + font-size: 20px; + font-weight: bold; + color: #FFFFFF; + text-shadow: 0 1px 0 #ffffff; + /* opacity: 0.8; */ +} + +.toast-close-button:hover, +.toast-close-button:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.4; +} + +/*Additional properties for button version + iOS requires the button element instead of an anchor tag. + If you want the anchor version, it requires `href="#"`.*/ + +button.toast-close-button { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} + +.toast-container { + pointer-events: none; + position: fixed; + z-index: 999999; +} + +.toast-container * { + box-sizing: border-box; +} + +.toast-container .ngx-toastr { + position: relative; + overflow: hidden; + margin: 0 0 6px; + padding: 15px 15px 15px 50px; + width: 300px; + border-radius: 3px 3px 3px 3px; + background-position: 15px center; + background-repeat: no-repeat; + background-size: 24px; + box-shadow: 0 0 12px #999999; + color: #FFFFFF; +} + +.toast-container .ngx-toastr:hover { + box-shadow: 0 0 12px #000000; + opacity: 1; + cursor: pointer; +} + +/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/info-circle.svg */ + +.toast-info { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOS4wNDMgOCA4IDExOS4wODMgOCAyNTZjMCAxMzYuOTk3IDExMS4wNDMgMjQ4IDI0OCAyNDhzMjQ4LTExMS4wMDMgMjQ4LTI0OEM1MDQgMTE5LjA4MyAzOTIuOTU3IDggMjU2IDh6bTAgMTEwYzIzLjE5NiAwIDQyIDE4LjgwNCA0MiA0MnMtMTguODA0IDQyLTQyIDQyLTQyLTE4LjgwNC00Mi00MiAxOC44MDQtNDIgNDItNDJ6bTU2IDI1NGMwIDYuNjI3LTUuMzczIDEyLTEyIDEyaC04OGMtNi42MjcgMC0xMi01LjM3My0xMi0xMnYtMjRjMC02LjYyNyA1LjM3My0xMiAxMi0xMmgxMnYtNjRoLTEyYy02LjYyNyAwLTEyLTUuMzczLTEyLTEydi0yNGMwLTYuNjI3IDUuMzczLTEyIDEyLTEyaDY0YzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MTAwaDEyYzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MjR6Jy8+PC9zdmc+"); +} + +/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/times-circle.svg */ + +.toast-error { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOSA4IDggMTE5IDggMjU2czExMSAyNDggMjQ4IDI0OCAyNDgtMTExIDI0OC0yNDhTMzkzIDggMjU2IDh6bTEyMS42IDMxMy4xYzQuNyA0LjcgNC43IDEyLjMgMCAxN0wzMzggMzc3LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwyNTYgMzEybC02NS4xIDY1LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwxMzQuNCAzMzhjLTQuNy00LjctNC43LTEyLjMgMC0xN2w2NS42LTY1LTY1LjYtNjUuMWMtNC43LTQuNy00LjctMTIuMyAwLTE3bDM5LjYtMzkuNmM0LjctNC43IDEyLjMtNC43IDE3IDBsNjUgNjUuNyA2NS4xLTY1LjZjNC43LTQuNyAxMi4zLTQuNyAxNyAwbDM5LjYgMzkuNmM0LjcgNC43IDQuNyAxMi4zIDAgMTdMMzEyIDI1Nmw2NS42IDY1LjF6Jy8+PC9zdmc+"); +} + +/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/check.svg */ + +.toast-success { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTE3My44OTggNDM5LjQwNGwtMTY2LjQtMTY2LjRjLTkuOTk3LTkuOTk3LTkuOTk3LTI2LjIwNiAwLTM2LjIwNGwzNi4yMDMtMzYuMjA0YzkuOTk3LTkuOTk4IDI2LjIwNy05Ljk5OCAzNi4yMDQgMEwxOTIgMzEyLjY5IDQzMi4wOTUgNzIuNTk2YzkuOTk3LTkuOTk3IDI2LjIwNy05Ljk5NyAzNi4yMDQgMGwzNi4yMDMgMzYuMjA0YzkuOTk3IDkuOTk3IDkuOTk3IDI2LjIwNiAwIDM2LjIwNGwtMjk0LjQgMjk0LjQwMWMtOS45OTggOS45OTctMjYuMjA3IDkuOTk3LTM2LjIwNC0uMDAxeicvPjwvc3ZnPg=="); +} + +/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/exclamation-triangle.svg */ + +.toast-warning { + background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1NzYgNTEyJyB3aWR0aD0nNTc2JyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTU2OS41MTcgNDQwLjAxM0M1ODcuOTc1IDQ3Mi4wMDcgNTY0LjgwNiA1MTIgNTI3Ljk0IDUxMkg0OC4wNTRjLTM2LjkzNyAwLTU5Ljk5OS00MC4wNTUtNDEuNTc3LTcxLjk4N0wyNDYuNDIzIDIzLjk4NWMxOC40NjctMzIuMDA5IDY0LjcyLTMxLjk1MSA4My4xNTQgMGwyMzkuOTQgNDE2LjAyOHpNMjg4IDM1NGMtMjUuNDA1IDAtNDYgMjAuNTk1LTQ2IDQ2czIwLjU5NSA0NiA0NiA0NiA0Ni0yMC41OTUgNDYtNDYtMjAuNTk1LTQ2LTQ2LTQ2em0tNDMuNjczLTE2NS4zNDZsNy40MTggMTM2Yy4zNDcgNi4zNjQgNS42MDkgMTEuMzQ2IDExLjk4MiAxMS4zNDZoNDguNTQ2YzYuMzczIDAgMTEuNjM1LTQuOTgyIDExLjk4Mi0xMS4zNDZsNy40MTgtMTM2Yy4zNzUtNi44NzQtNS4wOTgtMTIuNjU0LTExLjk4Mi0xMi42NTRoLTYzLjM4M2MtNi44ODQgMC0xMi4zNTYgNS43OC0xMS45ODEgMTIuNjU0eicvPjwvc3ZnPg=="); +} + +.toast-container.toast-top-center .ngx-toastr, +.toast-container.toast-bottom-center .ngx-toastr { + width: 300px; + margin-left: auto; + margin-right: auto; +} + +.toast-container.toast-top-full-width .ngx-toastr, +.toast-container.toast-bottom-full-width .ngx-toastr { + width: 96%; + margin-left: auto; + margin-right: auto; +} + +.ngx-toastr { + background-color: #030303; + pointer-events: auto; +} + +.toast-success { + background-color: #51A351; +} + +.toast-error { + background-color: #BD362F; +} + +.toast-info { + background-color: #2F96B4; +} + +.toast-warning { + background-color: #F89406; +} + +.toast-progress { + position: absolute; + left: 0; + bottom: 0; + height: 4px; + background-color: #000000; + opacity: 0.4; +} + +/* Responsive Design */ + +@media all and (max-width: 240px) { + .toast-container .ngx-toastr.div { + padding: 8px 8px 8px 50px; + width: 11em; + } + .toast-container .toast-close-button { + right: -0.2em; + top: -0.2em; + } +} + +@media all and (min-width: 241px) and (max-width: 480px) { + .toast-container .ngx-toastr.div { + padding: 8px 8px 8px 50px; + width: 18em; + } + .toast-container .toast-close-button { + right: -0.2em; + top: -0.2em; + } +} + +@media all and (min-width: 481px) and (max-width: 768px) { + .toast-container .ngx-toastr.div { + padding: 15px 15px 15px 50px; + width: 25em; + } +} + diff --git a/src/Server/Coderr.Server.WebSite/wwwroot/styles.css.map b/src/Server/Coderr.Server.WebSite/wwwroot/styles.css.map new file mode 100644 index 00000000..c5280c18 --- /dev/null +++ b/src/Server/Coderr.Server.WebSite/wwwroot/styles.css.map @@ -0,0 +1 @@ +{"version":3,"file":"styles.css","mappings":";;;AAAA;EACI;EACA;EACA;EACA;ACCJ;;ADEA;EACI;ACCJ;;ADEA;EACI;EACA;EACA;ACCJ;;ADEA;EACI;ACCJ;;ADEA;EACI;EACA;ACCJ;;ACrBI;EACI;EACA;EACA;EACA;EACA;EACA,gBCPG;EDQH;EACA;EACA;ADwBR;;ACrBI;EACI,mBCLD;EDMC;ADuBR;;ACpBI;EACI;ADsBR;;ACnBI;EACI,mBCnBG;EDoBH;EACA;ADqBR;;AClBI;EACI;EACA;EACA;EACA;EACA;EACA;EACA;ADoBR;;ACjBI;EACI;ADmBR;;AChBI;EACI,qBC7CG;AF+DX;;AG5DE;EACE;AH+DJ;;AG3DA;EACE;AH8DF;;AG5DE;EACE,yBDGE;ECFF;EACA,cDbO;AF2EX;;AG3DE;EACE,yBDLM;ECMN;AH6DJ;;AI7EI;EACI;EACA;EACA;EACA;EACA;EACA,gBFPG;EEQH;EACA;EACA;AJgFR;;AI7EI;EACI,mBFLD;EEMC;AJ+ER;;AI5EI;EACI;AJ8ER;;AI3EI;EACI,mBFnBG;EEoBH;EACA;AJ6ER;;AI1EI;EACI;EACA;EACA;EACA;EACA;EACA;EACA;AJ4ER;;AIzEI;EACI;AJ2ER;;AIxEI;EACI,qBF7CG;AFuHX;;AKrHA;EACE;ALwHF;;AKrHA;EACE;ALwHF;;AKrHA;EACE;EACA;EACA;ALwHF;;AKpHE;EACE;ALuHJ;;AKpHE;EACE;EACA;ALsHJ;;AKpHI;EACE;ALsHN;;AKjHI;EACE;ALmHN;;AK/GE;;;;;;EAME,yBH9BM;EG+BN;EACA;EACA;EACA;EACA;ALiHJ;;AK9GE;EACE;EACA;ALgHJ;;AK7GE;EACE;AL+GJ;;AK5GE;EAEE,yBHlDG;EGmDH;EACA,cH9DO;EG+DP;AL6GJ;;AK1GE;EAEE,cHpEO;EGqEP;EACA;AL2GJ;;AMjLA;EACE;EACA;EACA;EACA;ANoLF;;AMjLA;EACE;EACA;EACA;EACA;EAMA;AN+KF;;AMnLE;EACE;ANqLJ;;AM/KA;EACE;EACA;EACA;EACA;sBAAA;EAEA;EACA;EAEA;EACA;ANiLF;;AM9KA;EACE;ANiLF;;AM9KA;EACE;ANiLF;;AM9KA;EACE;EACA;ANiLF;;AM9KA;EACE;ANiLF;;AM9KA;EACE;IACE;IACA;ENiLF;AACF;;AM9KA;EACE;ANgLF;;AM7KA;EACE;ANgLF;;AM7KA;EACE;ANgLF;;AM5KE;EACE;AN+KJ;;AM5KE;EACE;AN+KJ;;AM5KE;EACE;AN+KJ;;AM5KE;EACE;AN+KJ;;AM5KE;EACE;AN+KJ;;AM5KE;EACE;AN+KJ;;AM3KE;EACE;AN8KJ;;AM1KE;EACE;AN6KJ;;AM1KE;EACE;AN6KJ;;AMhNE;EACE;ANmNJ;;AMhNE;EACE;ANmNJ;;AMhNE;EACE;ANmNJ;;AMhNE;EACE;ANmNJ;;AMhNE;EACE;ANmNJ;;AMhNE;EACE;ANmNJ;;AM/ME;EACE;ANkNJ;;AM9ME;EACE;ANiNJ;;AM9ME;EACE;ANiNJ;;AMpPE;EACE;ANuPJ;;AMpPE;EACE;ANuPJ;;AMpPE;EACE;ANuPJ;;AMpPE;EACE;ANuPJ;;AMpPE;EACE;ANuPJ;;AMpPE;EACE;ANuPJ;;AMnPE;EACE;ANsPJ;;AMlPE;EACE;ANqPJ;;AMlPE;EACE;ANqPJ;;AMxRE;EACE;AN2RJ;;AMxRE;EACE;AN2RJ;;AMxRE;EACE;AN2RJ;;AMxRE;EACE;AN2RJ;;AMxRE;EACE;AN2RJ;;AMxRE;EACE;AN2RJ;;AMvRE;EACE;AN0RJ;;AMtRE;EACE;ANyRJ;;AMtRE;EACE;ANyRJ;;AM5TE;EACE;AN+TJ;;AM5TE;EACE;AN+TJ;;AM5TE;EACE;AN+TJ;;AM5TE;EACE;AN+TJ;;AM5TE;EACE;AN+TJ;;AM5TE;EACE;AN+TJ;;AM3TE;EACE;AN8TJ;;AM1TE;EACE;AN6TJ;;AM1TE;EACE;AN6TJ;;AMhWE;EACE;ANmWJ;;AMhWE;EACE;ANmWJ;;AMhWE;EACE;ANmWJ;;AMhWE;EACE;ANmWJ;;AMhWE;EACE;ANmWJ;;AMhWE;EACE;ANmWJ;;AM/VE;EACE;ANkWJ;;AM9VE;EACE;ANiWJ;;AM9VE;EACE;ANiWJ;;AOvcE;EACE,yBLJO;EKKP;EACA;AP0cJ;;AOvcE;EACE;EACA,qBLDG;EKEH,cLZO;EKaP;APycJ;;AOtcE;EAEE,yBLlBO;EKmBP,qBLnBO;EKoBP;EACA,cLhBO;EKiBP;APucJ;;AOpcE;EAEE,cL3BO;EK4BP,yBLdE;EKeF,qBLfE;EKgBF;APqcJ;;AOhcA;;;;;;;EAGE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;APucF;;AOrcE;;;;;;;EACE;EACA;EACA;EACA;AP6cJ;;AO1cE;;;;;;;EACE,iBAzDQ;EA0DR,cLrDO;AFugBX;;AO/cE;;;;;;;EACE;EACA;APudJ;;AOpdE;;;;;;;EACE,yBLnEO;EKoEP,cL/DO;AF2hBX;;AOzdE;;;;;;;EACE,yBLnEO;EKoEP,cLzEO;AF0iBX;;AO9dE;;;;;;;EACE;EACA;EACA;EACA;APseJ;;AOneE;;;;;;;EACE;AP2eJ;;AOxeE;;;;;;;;;;;;;EACE;EACA;EACA;APsfJ;;AOnfE;;;;;;;EACE;EACA;EACA;AP2fJ;;AOxfE;;;;;;;EACE;EACA;EACA;APggBJ;;AO7fE;;;;;;;EACE;EACA,cLrGO;EKsGP;APqgBJ;;AOlgBE;;;;;;;EACE;EACA,cL1GO;EK2GP;AP0gBJ;;AOxgBI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EACE,mBL3GC;AF2sBP;;AO5lBE;;;;;;;EACE;EACA,cL3HO;AF+tBX;;AQ9tBA;EACE,mBNKS;EMJT;EACA;EACA;;;GAAA;ARouBF;;AQhuBE;EACE;EACA;EACA;ARkuBJ;;AQ/tBE;EACE;EACA;ARiuBJ;;AQ/tBI;EACE,cNlBK;EMmBL;EACA;ARiuBN;;AQ/tBM;EACE;EACA,cNxBG;AFyvBX;;AQ9tBM;EACE;EACA,cNrBD;AFqvBP;;AQ3tBE;EACE;AR6tBJ;;AQ1tBE;EACE;EACA;EACA;EACA;AR4tBJ;;AQ1tBI;EACE;EACA;EACA;EACA;EACA;AR4tBN;;AQxtBE;EACE;AR0tBJ;;AQvtBE;EACE,mBNtDO;EMuDP;EACA;ARytBJ;;AQvtBI;EACE;EACA;ARytBN;;AQvtBM;EACE,yBN1BM;EM2BN;EACA;EACA;ARytBR;;AQrtBI;EACE,cN3EK;EM4EL;ARutBN;;AQptBI;EACE;EACA;ARstBN;;AQntBI;EACE;ARqtBN;;AS5yBI;EACI;AT+yBR;;AS5yBI;EACI;AT8yBR;;AUjzBA;EACI;AVozBJ;;AUlzBA;EACI;AVqzBJ;;AUlzBA;EAEI,cRGE;EQFF;AVozBJ;;AUjzBA;EACI;EACA;AVozBJ;;AUjzBA;EChBE;AXq0BF;;AUjzBA;EACI;EACA;EACA;AVozBJ;;AUjzBA;EACI;AVozBJ;;AUjzBA;EACI;AVozBJ;;AUlzBA;EACE;AVqzBF;;AUlzBA;EACE,cRhCK;AFq1BP;;AUlzBA;EACI,cRzCO;AF81BX;;AUlzBA;EACI,cRlDO;AFu2BX;;AUlzBA;EACI,cRxCE;AF61BN;;AUlzBA;EACI;AVqzBJ;;AUlzBA;EACI;AVqzBJ;;AYl3BA;EACE;EACA;AZq3BF;;AYn3BE;EACE,sBV4BW;AFy1Bf;;AYn3BI;EACE;EACA;EACA;AZq3BN;;AYh3BI;EACE;AZk3BN;;AY92BE;EAEE;EACA;AZ+2BJ;;AAz3BA;EACE,yBELK;AFi4BP;;AAz3BA;EACE;AA43BF;;AAz3BA;EACE;EACA;AA43BF;;AA13BE;EACE;EACA;AA43BJ;;AAx3BA;EACE;AA23BF;;AAx3BA;EACE;AA23BF;;AAx3BA;EACE;EACA;AA23BF;;AAx3BA;EACE;AA23BF;;AAx3BA;EACE;EACA;EACA;AA23BF;;AAz3BE;EACE;EACA;EACA;EACA;EACA;AA23BJ;;AAv3BA;EACE,cEzDS;EF0DT;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AA03BF;;AAx3BE;EACE;EACA;AA03BJ;;AAv3BE;EACE;AAy3BJ;;AAt3BE;EACE;EWhFF;AXy8BF;;AAr3BE;EACE,yBElEO;ESvBT;EX2FE;EACA;EACA;EACA;AAu3BJ;;AAr3BI;EACE,cE5FK;AFm9BX;;AAn3BE;EACE,yBE/EO;EFgFP;AAq3BJ;;AAl3BE;EACE;EACA;EACA;AAo3BJ;;AAj3BE;EACE;EACA;EACA;AAm3BJ;;AAh3BE;EACE;EACA;AAk3BJ;;AA72BA;EACE;EACA;AAg3BF;;AA72BA;EACE;AAg3BF;;AA32BE;EACE;EACA;EACA;AA82BJ;;AA12BA;EACE;EACA;AA62BF;;AA32BE;EACE;EACA;EACA;EACA;EACA;EACA;EACA;AA62BJ;;AA32BI;EACE;AA62BN;;AAx2BA;EACE;AA22BF;;AAx2BA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AA22BF;;AAz2BE;EACE;EACA;AA22BJ;;AAv2BA;EACE;EACA;AA02BF;;AAt2BA;;AAEA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AAw2BF;;AAr2BA;EACE;EACA;EACA,mBEnMQ;EFoMR;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;AAw2BF;;AAt2BE;EACE;AAw2BJ;;AAr2BE;EACE;EACA;EACA;EACA,mBE1NG;EF2NH;EACA;EACA;EACA;EACA;AAu2BJ;;AAp2BE;EACE;AAs2BJ;;AAl2BA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;AAq2BF;;AAn2BE;EACE;EACA;AAq2BJ,C;;;;AalmCA,kJAAkJ;;AAElJ,aAAa;;AACb;EACE,QAAQ;EACR,SAAS;EACT,gCAAgC;AAClC;;AACA;EACE,MAAM;EACN,QAAQ;EACR,WAAW;AACb;;AACA;EACE,SAAS;EACT,QAAQ;EACR,WAAW;AACb;;AACA;EACE,MAAM;EACN,QAAQ;EACR,WAAW;AACb;;AACA;EACE,SAAS;EACT,QAAQ;EACR,WAAW;AACb;;AACA;EACE,SAAS;EACT,UAAU;AACZ;;AACA;EACE,SAAS;EACT,WAAW;AACb;;AACA;EACE,WAAW;EACX,YAAY;AACd;;AACA;EACE,YAAY;EACZ,UAAU;AACZ;;AAEA,iBAAiB;;AACjB;EACE,iBAAiB;AACnB;;AACA;EACE,qBAAqB;AACvB;;AACA;;EAEE,cAAc;AAChB;;AACA;EACE,cAAc;EACd,qBAAqB;AACvB;;AACA;EACE,kBAAkB;EAClB,aAAa;EACb,WAAW;EACX,YAAY;EACZ,eAAe;EACf,iBAAiB;EACjB,cAAc;EACd,4BAA4B;EAC5B,kBAAkB;AACpB;;AACA;;EAEE,cAAc;EACd,qBAAqB;EACrB,eAAe;EACf,YAAY;AACd;;AACA;;yDAEyD;;AACzD;EACE,UAAU;EACV,eAAe;EACf,uBAAuB;EACvB,SAAS;AACX;;AACA;EACE,oBAAoB;EACpB,eAAe;EACf,eAAe;AACjB;;AACA;EACE,sBAAsB;AACxB;;AACA;EACE,kBAAkB;EAClB,gBAAgB;EAChB,eAAe;EACf,4BAA4B;EAC5B,YAAY;EACZ,8BAA8B;EAC9B,gCAAgC;EAChC,4BAA4B;EAC5B,qBAAqB;EACrB,4BAA4B;EAC5B,cAAc;AAChB;;AACA;EACE,4BAA4B;EAC5B,UAAU;EACV,eAAe;AACjB;;AACA,iHAAiH;;AACjH;EACE,mvBAAmvB;AACrvB;;AACA,kHAAkH;;AAClH;EACE,mtBAAmtB;AACrtB;;AACA,2GAA2G;;AAC3G;EACE,+kBAA+kB;AACjlB;;AACA,0HAA0H;;AAC1H;EACE,uzBAAuzB;AACzzB;;AACA;;EAEE,YAAY;EACZ,iBAAiB;EACjB,kBAAkB;AACpB;;AACA;;EAEE,UAAU;EACV,iBAAiB;EACjB,kBAAkB;AACpB;;AACA;EACE,yBAAyB;EACzB,oBAAoB;AACtB;;AACA;EACE,yBAAyB;AAC3B;;AACA;EACE,yBAAyB;AAC3B;;AACA;EACE,yBAAyB;AAC3B;;AACA;EACE,yBAAyB;AAC3B;;AACA;EACE,kBAAkB;EAClB,OAAO;EACP,SAAS;EACT,WAAW;EACX,yBAAyB;EACzB,YAAY;AACd;;AACA,sBAAsB;;AACtB;EACE;IACE,yBAAyB;IACzB,WAAW;EACb;EACA;IACE,aAAa;IACb,WAAW;EACb;AACF;;AACA;EACE;IACE,yBAAyB;IACzB,WAAW;EACb;EACA;IACE,aAAa;IACb,WAAW;EACb;AACF;;AACA;EACE;IACE,4BAA4B;IAC5B,WAAW;EACb;AACF","sources":["./src/styles/_partials/reset.scss","./src/styles/site.scss","./src/styles/_partials/radio.scss","./src/styles/_partials/coderr-variables.scss","./src/styles/_partials/backgrounds.scss","./src/styles/_partials/checkbox.scss","./src/styles/_partials/forms.scss","./src/styles/_partials/layout.scss","./src/styles/_partials/buttons.scss","./src/styles/_partials/topnav.scss","./src/styles/_partials/image.scss","./src/styles/_partials/typography.scss","./src/styles/_partials/_mixins.scss","./src/styles/_partials/tables.scss","./node_modules/ngx-toastr/toastr.css"],"sourcesContent":["html {\r\n box-sizing: border-box;\r\n font-size: 15px;\r\n font-family: \"Sofia Pro\", sans-serif;\r\n height: 100%;\r\n}\r\n\r\n*, *:before, *:after {\r\n box-sizing: inherit;\r\n}\r\n\r\nbody, h1, h2, h3, h4, h5, h6, p, ol, ul {\r\n margin: 0;\r\n padding: 0;\r\n font-weight: normal;\r\n}\r\n\r\nol, ul {\r\n list-style: none;\r\n}\r\n\r\nimg {\r\n max-width: 100%;\r\n height: auto;\r\n}\r\n","html {\n box-sizing: border-box;\n font-size: 15px;\n font-family: \"Sofia Pro\", sans-serif;\n height: 100%;\n}\n\n*, *:before, *:after {\n box-sizing: inherit;\n}\n\nbody, h1, h2, h3, h4, h5, h6, p, ol, ul {\n margin: 0;\n padding: 0;\n font-weight: normal;\n}\n\nol, ul {\n list-style: none;\n}\n\nimg {\n max-width: 100%;\n height: auto;\n}\n\n.card input[type=radio] {\n cursor: pointer;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n outline: 0;\n background: #eee;\n height: 16px;\n width: 16px;\n border: 1px solid white;\n}\n.card input[type=radio]:checked {\n background: #59c1d5;\n color: #59c1d5 !important;\n}\n.card input[type=radio]:hover {\n filter: brightness(90%);\n}\n.card input[type=radio]:disabled {\n background: #393938;\n opacity: 0.6;\n pointer-events: none;\n}\n.card input[type=radio]:after {\n content: \"\";\n position: relative;\n left: 40%;\n top: 20%;\n width: 15%;\n height: 40%;\n display: none;\n}\n.card input[type=radio]:checked:after {\n display: block;\n}\n.card input[type=radio]:disabled:after {\n border-color: #f4f4f4;\n}\n\n.bg.white {\n background-color: white;\n}\n\n.alert {\n padding: 10px;\n}\n.alert.warning {\n background-color: #f18c65;\n border: 1px solid #ed6936;\n color: #ededed;\n}\n.alert.success {\n background-color: #d8eef4;\n border: 1px solid #b0dde9;\n}\n\n.card input[type=checkbox] {\n cursor: pointer;\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n outline: 0;\n background: #eee;\n height: 16px;\n width: 16px;\n border: 1px solid white;\n}\n.card input[type=checkbox]:checked {\n background: #59c1d5;\n color: #59c1d5 !important;\n}\n.card input[type=checkbox]:hover {\n filter: brightness(90%);\n}\n.card input[type=checkbox]:disabled {\n background: #393938;\n opacity: 0.6;\n pointer-events: none;\n}\n.card input[type=checkbox]:after {\n content: \"\";\n position: relative;\n left: 40%;\n top: 20%;\n width: 15%;\n height: 40%;\n display: none;\n}\n.card input[type=checkbox]:checked:after {\n display: block;\n}\n.card input[type=checkbox]:disabled:after {\n border-color: #f4f4f4;\n}\n\ninput:invalid {\n box-shadow: 0 0 5px 1px #f18c65;\n}\n\ninput:focus:invalid {\n box-shadow: none;\n}\n\ninput, select, textarea {\n padding: 4px;\n border: 1px solid #404040;\n border-radius: 3px;\n}\n\n.form .form-group {\n margin-top: 15px;\n}\n.form label {\n display: block;\n font-weight: bold;\n}\n.form label.inline {\n display: inline;\n}\n.form input.inline {\n display: inline;\n}\n.form input[type=text],\n.form input[type=number],\n.form input[type=email],\n.form input[type=password],\n.form textarea,\n.form select {\n background-color: #abdbe7;\n border: 1px solid #216272;\n border-radius: 3px;\n padding: 8px;\n border: #5cb9d0;\n width: 100%;\n}\n.form textarea {\n min-height: 100px;\n width: 100%;\n}\n.form button {\n display: inline-flex;\n}\n.form button[type=submit], .form a.submit {\n background-color: #59c1d5;\n border: #288ca0;\n color: #ededed;\n width: inherit;\n}\n.form button[type=reset], .form a.reset {\n color: #ededed;\n background-color: #33b0c8;\n border: #dc4c14;\n}\n\n.row {\n display: flex;\n width: 100%;\n flex-flow: row wrap;\n max-width: 100%;\n}\n\n.container {\n display: block;\n max-width: 1400px;\n margin: 0 auto;\n padding: 30px;\n padding: -10px;\n}\n.container h1 {\n font-size: 48px;\n}\n\n.col {\n justify-content: center;\n align-items: start;\n flex-basis: 0;\n /*flex-direction: column;\n flex-basis: auto;*/\n flex: 1 1 200px;\n margin: 10px;\n /* pre > code wont wrap otherwise */\n min-width: 0;\n}\n\n.col-2 {\n flex: 2;\n}\n\n.col-3 {\n flex: 3;\n}\n\n.hidden {\n display: none;\n opacity: 0;\n}\n\n.w-100 {\n width: 100%;\n}\n\n@media only screen and (max-width: 600px) {\n .col {\n display: block;\n width: 100%;\n }\n}\n.y-center {\n align-items: center;\n}\n\n.x-center {\n justify-content: center;\n}\n\n.mx-auto {\n margin: 0 auto;\n}\n\n.mt-0 {\n margin-top: 0px;\n}\n\n.mb-0 {\n margin-bottom: 0px;\n}\n\n.ml-0 {\n margin-left: 0px !important;\n}\n\n.m-0 {\n margin: 0px;\n}\n\n.pt-0 {\n padding-top: 0px;\n}\n\n.pl-0 {\n padding-left: 0px;\n}\n\n.pr-0 {\n padding-right: 0px;\n}\n\n.pb-0 {\n padding-bottom: 0px;\n}\n\n.p-0 {\n padding: 0px;\n}\n\n.mt-1 {\n margin-top: 5px;\n}\n\n.mb-1 {\n margin-bottom: 5px;\n}\n\n.ml-1 {\n margin-left: 5px !important;\n}\n\n.m-1 {\n margin: 5px;\n}\n\n.pt-1 {\n padding-top: 5px;\n}\n\n.pl-1 {\n padding-left: 5px;\n}\n\n.pr-1 {\n padding-right: 5px;\n}\n\n.pb-1 {\n padding-bottom: 5px;\n}\n\n.p-1 {\n padding: 5px;\n}\n\n.mt-2 {\n margin-top: 10px;\n}\n\n.mb-2 {\n margin-bottom: 10px;\n}\n\n.ml-2 {\n margin-left: 10px !important;\n}\n\n.m-2 {\n margin: 10px;\n}\n\n.pt-2 {\n padding-top: 10px;\n}\n\n.pl-2 {\n padding-left: 10px;\n}\n\n.pr-2 {\n padding-right: 10px;\n}\n\n.pb-2 {\n padding-bottom: 10px;\n}\n\n.p-2 {\n padding: 10px;\n}\n\n.mt-3 {\n margin-top: 15px;\n}\n\n.mb-3 {\n margin-bottom: 15px;\n}\n\n.ml-3 {\n margin-left: 15px !important;\n}\n\n.m-3 {\n margin: 15px;\n}\n\n.pt-3 {\n padding-top: 15px;\n}\n\n.pl-3 {\n padding-left: 15px;\n}\n\n.pr-3 {\n padding-right: 15px;\n}\n\n.pb-3 {\n padding-bottom: 15px;\n}\n\n.p-3 {\n padding: 15px;\n}\n\n.mt-4 {\n margin-top: 20px;\n}\n\n.mb-4 {\n margin-bottom: 20px;\n}\n\n.ml-4 {\n margin-left: 20px !important;\n}\n\n.m-4 {\n margin: 20px;\n}\n\n.pt-4 {\n padding-top: 20px;\n}\n\n.pl-4 {\n padding-left: 20px;\n}\n\n.pr-4 {\n padding-right: 20px;\n}\n\n.pb-4 {\n padding-bottom: 20px;\n}\n\n.p-4 {\n padding: 20px;\n}\n\n.mt-5 {\n margin-top: 25px;\n}\n\n.mb-5 {\n margin-bottom: 25px;\n}\n\n.ml-5 {\n margin-left: 25px !important;\n}\n\n.m-5 {\n margin: 25px;\n}\n\n.pt-5 {\n padding-top: 25px;\n}\n\n.pl-5 {\n padding-left: 25px;\n}\n\n.pr-5 {\n padding-right: 25px;\n}\n\n.pb-5 {\n padding-bottom: 25px;\n}\n\n.p-5 {\n padding: 25px;\n}\n\n.panel .btn, .panel button[type=reset], .panel a.reset, .panel button[type=submit], .panel a.submit {\n background-color: #ededed;\n color: #393938;\n display: inline-flex;\n}\n.panel.fill .btn, .panel.fill button[type=reset], .panel.fill a.reset, .panel.fill button[type=submit], .panel.fill a.submit, .panel .fill .btn, .panel .fill button[type=reset], .panel .fill a.reset, .panel .fill button[type=submit], .panel .fill a.submit {\n background-color: #59c1d5;\n border-color: #59c1d5;\n color: #ededed;\n display: inline-flex;\n}\n.panel button[type=submit], .panel a.submit {\n background-color: #ededed;\n border-color: #ededed;\n border: #d4d4d4;\n color: #393938;\n width: inherit;\n}\n.panel button[type=reset], .panel a.reset {\n color: #ededed;\n background-color: #f18c65;\n border-color: #f18c65;\n border: #ed6936;\n}\n\na.btn, .form a.submit, .form a.reset, .panel a.submit, .panel a.reset,\nbutton.btn,\n.form button[type=submit],\n.form button[type=reset],\n.panel button[type=submit],\n.panel button[type=reset],\ninput.button {\n padding: 10px 12px;\n margin-top: 15px;\n margin-right: 5px;\n border-radius: 3px;\n text-decoration: none;\n box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);\n text-shadow: 1px 1px rgba(0, 0, 0, 0.05);\n text-align: center;\n cursor: pointer;\n display: inline-block;\n}\na.btn.small, .form a.small.submit, .form a.small.reset, .panel a.small.submit, .panel a.small.reset,\nbutton.btn.small,\n.form button.small[type=submit],\n.form button.small[type=reset],\n.panel button.small[type=submit],\n.panel button.small[type=reset],\ninput.button.small {\n margin-top: 5px;\n margin-right: 2px;\n padding: 4px 6px;\n font-size: 0.9em;\n}\na.btn.default, .form a.default.submit, .form a.default.reset, .panel a.default.submit, .panel a.default.reset,\nbutton.btn.default,\n.form button.default[type=submit],\n.form button.default[type=reset],\n.panel button.default[type=submit],\n.panel button.default[type=reset],\ninput.button.default {\n background: white;\n color: #393938;\n}\na.btn.block, .form a.block.submit, .form a.block.reset, .panel a.block.submit, .panel a.block.reset,\nbutton.btn.block,\n.form button.block[type=submit],\n.form button.block[type=reset],\n.panel button.block[type=submit],\n.panel button.block[type=reset],\ninput.button.block {\n display: block;\n width: 100%;\n}\na.btn.light, .form a.light.submit, .form a.light.reset, .panel a.light.submit, .panel a.light.reset,\nbutton.btn.light,\n.form button.light[type=submit],\n.form button.light[type=reset],\n.panel button.light[type=submit],\n.panel button.light[type=reset],\ninput.button.light {\n background-color: #ededed;\n color: #393938;\n}\na.btn.dark, .form a.dark.submit, .form a.dark.reset, .panel a.dark.submit, .panel a.dark.reset,\nbutton.btn.dark,\n.form button.dark[type=submit],\n.form button.dark[type=reset],\n.panel button.dark[type=submit],\n.panel button.dark[type=reset],\ninput.button.dark {\n background-color: #393938;\n color: #ededed;\n}\na.btn.red, .form a.red.submit, .form a.red.reset, .panel a.red.submit, .panel a.red.reset,\nbutton.btn.red,\n.form button.red[type=submit],\n.form button.red[type=reset],\n.panel button.red[type=submit],\n.panel button.red[type=reset],\ninput.button.red {\n background-color: #f18c65 !important;\n color: white;\n border: 1px solid #f18960;\n transition: 0.5s;\n}\na.btn.red:hover, .form a.red.submit:hover, .form a.red.reset:hover, .panel a.red.submit:hover, .panel a.red.reset:hover,\nbutton.btn.red:hover,\n.form button.red[type=submit]:hover,\n.form button.red[type=reset]:hover,\n.panel button.red[type=submit]:hover,\n.panel button.red[type=reset]:hover,\ninput.button.red:hover {\n background-color: #eb581f !important;\n}\na.btn.blue, .form a.blue.submit, .form a.blue.reset, .panel a.blue.submit, .panel a.blue.reset, a.btn.default, .form a.default.submit, .form a.default.reset, .panel a.default.submit, .panel a.default.reset,\nbutton.btn.blue,\n.form button.blue[type=submit],\n.form button.blue[type=reset],\n.panel button.blue[type=submit],\n.panel button.blue[type=reset],\nbutton.btn.default,\n.form button.default[type=submit],\n.form button.default[type=reset],\n.panel button.default[type=submit],\n.panel button.default[type=reset],\ninput.button.blue,\ninput.button.default {\n background-color: #59c1d5 !important;\n color: white;\n border: 1px solid #55bfd4;\n}\na.btn.red50, .form a.red50.submit, .form a.red50.reset, .panel a.red50.submit, .panel a.red50.reset,\nbutton.btn.red50,\n.form button.red50[type=submit],\n.form button.red50[type=reset],\n.panel button.red50[type=submit],\n.panel button.red50[type=reset],\ninput.button.red50 {\n background-color: rgba(241, 140, 101, 0.75) !important;\n color: white;\n border: 1px solid rgba(241, 140, 101, 0.95);\n}\na.btn.red-2, .form a.red-2.submit, .form a.red-2.reset, .panel a.red-2.submit, .panel a.red-2.reset,\nbutton.btn.red-2,\n.form button.red-2[type=submit],\n.form button.red-2[type=reset],\n.panel button.red-2[type=submit],\n.panel button.red-2[type=reset],\ninput.button.red-2 {\n background-color: #f39d7c !important;\n color: white;\n border: 1px solid rgba(241, 140, 101, 0.5);\n}\na.btn.white, .form a.white.submit, .form a.white.reset, .panel a.white.submit, .panel a.white.reset,\nbutton.btn.white,\n.form button.white[type=submit],\n.form button.white[type=reset],\n.panel button.white[type=submit],\n.panel button.white[type=reset],\ninput.button.white {\n background-color: white !important;\n color: #2e2d2c;\n border: 1px solid #fcfcfc;\n}\na.btn.gray, .form a.gray.submit, .form a.gray.reset, .panel a.gray.submit, .panel a.gray.reset,\nbutton.btn.gray,\n.form button.gray[type=submit],\n.form button.gray[type=reset],\n.panel button.gray[type=submit],\n.panel button.gray[type=reset],\ninput.button.gray {\n background-color: #eee !important;\n color: #141414;\n border: 1px solid #fcfcfc;\n}\na.btn.gray .btn, .form a.gray.submit .btn, .form a.gray.reset .btn, .panel a.gray.submit .btn, .panel a.gray.reset .btn, a.btn.gray .form button[type=submit], .form a.btn.gray button[type=submit], .form a.gray.submit button[type=submit], .form a.gray.reset button[type=submit], .panel a.gray.submit .form button[type=submit], .form .panel a.gray.submit button[type=submit], .panel a.gray.reset .form button[type=submit], .form .panel a.gray.reset button[type=submit], a.btn.gray .form a.submit, .form a.btn.gray a.submit, .form a.gray.submit a.submit, .form a.gray.reset a.submit, .panel a.gray.submit .form a.submit, .form .panel a.gray.submit a.submit, .panel a.gray.reset .form a.submit, .form .panel a.gray.reset a.submit, a.btn.gray .form button[type=reset], .form a.btn.gray button[type=reset], .form a.gray.submit button[type=reset], .form a.gray.reset button[type=reset], .panel a.gray.submit .form button[type=reset], .form .panel a.gray.submit button[type=reset], .panel a.gray.reset .form button[type=reset], .form .panel a.gray.reset button[type=reset], a.btn.gray .form a.reset, .form a.btn.gray a.reset, .form a.gray.submit a.reset, .form a.gray.reset a.reset, .panel a.gray.submit .form a.reset, .form .panel a.gray.submit a.reset, .panel a.gray.reset .form a.reset, .form .panel a.gray.reset a.reset, a.btn.gray .panel button[type=submit], .panel a.btn.gray button[type=submit], .form a.gray.submit .panel button[type=submit], .panel .form a.gray.submit button[type=submit], .form a.gray.reset .panel button[type=submit], .panel .form a.gray.reset button[type=submit], .panel a.gray.submit button[type=submit], .panel a.gray.reset button[type=submit], a.btn.gray .panel a.submit, .panel a.btn.gray a.submit, .form a.gray.submit .panel a.submit, .panel .form a.gray.submit a.submit, .form a.gray.reset .panel a.submit, .panel .form a.gray.reset a.submit, .panel a.gray.submit a.submit, .panel a.gray.reset a.submit, a.btn.gray .panel button[type=reset], .panel a.btn.gray button[type=reset], .form a.gray.submit .panel button[type=reset], .panel .form a.gray.submit button[type=reset], .form a.gray.reset .panel button[type=reset], .panel .form a.gray.reset button[type=reset], .panel a.gray.submit button[type=reset], .panel a.gray.reset button[type=reset], a.btn.gray .panel a.reset, .panel a.btn.gray a.reset, .form a.gray.submit .panel a.reset, .panel .form a.gray.submit a.reset, .form a.gray.reset .panel a.reset, .panel .form a.gray.reset a.reset, .panel a.gray.submit a.reset, .panel a.gray.reset a.reset,\nbutton.btn.gray .btn,\n.form button.gray[type=submit] .btn,\n.form button.gray[type=reset] .btn,\n.panel button.gray[type=submit] .btn,\n.panel button.gray[type=reset] .btn,\nbutton.btn.gray .form button[type=submit],\n.form button.btn.gray button[type=submit],\n.form button.gray[type=submit] button[type=submit],\n.form button.gray[type=reset] button[type=submit],\n.panel button.gray[type=submit] .form button[type=submit],\n.form .panel button.gray[type=submit] button[type=submit],\n.panel button.gray[type=reset] .form button[type=submit],\n.form .panel button.gray[type=reset] button[type=submit],\nbutton.btn.gray .form a.submit,\n.form button.btn.gray a.submit,\n.form button.gray[type=submit] a.submit,\n.form button.gray[type=reset] a.submit,\n.panel button.gray[type=submit] .form a.submit,\n.form .panel button.gray[type=submit] a.submit,\n.panel button.gray[type=reset] .form a.submit,\n.form .panel button.gray[type=reset] a.submit,\nbutton.btn.gray .form button[type=reset],\n.form button.btn.gray button[type=reset],\n.form button.gray[type=submit] button[type=reset],\n.form button.gray[type=reset] button[type=reset],\n.panel button.gray[type=submit] .form button[type=reset],\n.form .panel button.gray[type=submit] button[type=reset],\n.panel button.gray[type=reset] .form button[type=reset],\n.form .panel button.gray[type=reset] button[type=reset],\nbutton.btn.gray .form a.reset,\n.form button.btn.gray a.reset,\n.form button.gray[type=submit] a.reset,\n.form button.gray[type=reset] a.reset,\n.panel button.gray[type=submit] .form a.reset,\n.form .panel button.gray[type=submit] a.reset,\n.panel button.gray[type=reset] .form a.reset,\n.form .panel button.gray[type=reset] a.reset,\nbutton.btn.gray .panel button[type=submit],\n.panel button.btn.gray button[type=submit],\n.form button.gray[type=submit] .panel button[type=submit],\n.panel .form button.gray[type=submit] button[type=submit],\n.form button.gray[type=reset] .panel button[type=submit],\n.panel .form button.gray[type=reset] button[type=submit],\n.panel button.gray[type=submit] button[type=submit],\n.panel button.gray[type=reset] button[type=submit],\nbutton.btn.gray .panel a.submit,\n.panel button.btn.gray a.submit,\n.form button.gray[type=submit] .panel a.submit,\n.panel .form button.gray[type=submit] a.submit,\n.form button.gray[type=reset] .panel a.submit,\n.panel .form button.gray[type=reset] a.submit,\n.panel button.gray[type=submit] a.submit,\n.panel button.gray[type=reset] a.submit,\nbutton.btn.gray .panel button[type=reset],\n.panel button.btn.gray button[type=reset],\n.form button.gray[type=submit] .panel button[type=reset],\n.panel .form button.gray[type=submit] button[type=reset],\n.form button.gray[type=reset] .panel button[type=reset],\n.panel .form button.gray[type=reset] button[type=reset],\n.panel button.gray[type=submit] button[type=reset],\n.panel button.gray[type=reset] button[type=reset],\nbutton.btn.gray .panel a.reset,\n.panel button.btn.gray a.reset,\n.form button.gray[type=submit] .panel a.reset,\n.panel .form button.gray[type=submit] a.reset,\n.form button.gray[type=reset] .panel a.reset,\n.panel .form button.gray[type=reset] a.reset,\n.panel button.gray[type=submit] a.reset,\n.panel button.gray[type=reset] a.reset,\ninput.button.gray .btn,\ninput.button.gray .form button[type=submit],\n.form input.button.gray button[type=submit],\ninput.button.gray .form a.submit,\n.form input.button.gray a.submit,\ninput.button.gray .form button[type=reset],\n.form input.button.gray button[type=reset],\ninput.button.gray .form a.reset,\n.form input.button.gray a.reset,\ninput.button.gray .panel button[type=submit],\n.panel input.button.gray button[type=submit],\ninput.button.gray .panel a.submit,\n.panel input.button.gray a.submit,\ninput.button.gray .panel button[type=reset],\n.panel input.button.gray button[type=reset],\ninput.button.gray .panel a.reset,\n.panel input.button.gray a.reset {\n background: #59c1d5;\n}\na.btn.dark, .form a.dark.submit, .form a.dark.reset, .panel a.dark.submit, .panel a.dark.reset,\nbutton.btn.dark,\n.form button.dark[type=submit],\n.form button.dark[type=reset],\n.panel button.dark[type=submit],\n.panel button.dark[type=reset],\ninput.button.dark {\n background-color: #2e2d2c !important;\n color: #ededed;\n}\n\nheader {\n background: #141414;\n margin: 0;\n padding: 5px;\n /*.main {\n box-shadow: 0 15px 5px lighten($nav-bg, 20%);\n }\n */\n}\nheader img {\n vertical-align: middle;\n height: 25px;\n margin-top: -5px;\n}\nheader ul {\n list-style-type: none;\n display: inline-block;\n}\nheader ul li {\n color: #dddddd;\n display: inline-block;\n margin-left: 20px;\n}\nheader ul li a {\n text-decoration: none;\n color: #dddddd;\n}\nheader ul li a:hover {\n text-shadow: 0px 0px 2px rgba(255, 255, 255, 0.1), 0px 2px 3px rgba(255, 255, 255, 0.5), 0px 6px 6px rgba(255, 255, 255, 0.2);\n color: #59c1d5;\n}\nheader .box-shadow {\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.05);\n}\nheader .left-menu {\n flex-grow: 1;\n align-self: center;\n padding: 5px;\n color: #999;\n}\nheader .left-menu a {\n padding-left: 5px;\n padding-right: 5px;\n color: white;\n text-decoration: none;\n font-size: 14px;\n}\nheader .right-menu {\n align-self: center;\n}\nheader .submenu {\n background: #2e2d2c;\n padding-top: 10px;\n padding-left: 60px;\n}\nheader .submenu .groups {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nheader .submenu .groups a {\n background-color: #1e6977;\n padding: 5px;\n border-top-left-radius: 5px;\n border-top-right-radius: 5px;\n}\nheader .submenu a {\n color: #dddddd;\n text-decoration: none;\n}\nheader .submenu .application-list {\n display: flex;\n flex-direction: column;\n}\nheader .submenu .application-list div {\n padding: 5px;\n}\n\n.svg.blue {\n filter: invert(77%) sepia(17%) saturate(1201%) hue-rotate(144deg) brightness(89%) contrast(87%);\n}\n.svg.red {\n filter: invert(57%) sepia(89%) saturate(365%) hue-rotate(326deg) brightness(98%) contrast(92%);\n}\n\nspan.muted {\n color: #706f6f;\n}\n\nspan.small {\n font-size: 0.9em;\n}\n\na {\n color: #f18c65;\n text-decoration: none;\n}\n\np {\n margin-top: 5px;\n margin-bottom: 15px;\n}\n\n.text-shadow-1 {\n text-shadow: 2px 4px 3px rgba(0, 0, 0, 0.3);\n}\n\nh2, h3, h4 {\n font-weight: 600;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n\n.text-center {\n text-align: center;\n}\n\n.text-right {\n text-align: right;\n}\n\n.text-left {\n text-align: left;\n}\n\n.text-blue {\n color: #59c1d5;\n}\n\n.text-dark {\n color: #393938;\n}\n\n.text-light {\n color: #ededed;\n}\n\n.text-red {\n color: #f18c65;\n}\n\n.text-white {\n color: #fff;\n}\n\n.text-muted {\n color: #bbb;\n}\n\n.table {\n table-layout: fixed;\n display: table;\n}\n.table thead tr {\n background-color: #fff;\n}\n.table thead tr th {\n text-align: left;\n padding-top: 5px;\n padding-bottom: 5px;\n}\n.table.striped tbody tr:nth-child(even) {\n background-color: #bfe7ef;\n}\n.table tbody tr td {\n padding-top: 5px;\n padding-bottom: 5px;\n}\n\nbody {\n background-color: #59c1d5;\n}\n\n.main-view {\n /**padding: 10px; Let all views control this **/\n}\n\n.starter {\n margin: 10px;\n padding: 0;\n}\n.starter > .panel, .starter > .panels {\n margin: -10px !important;\n padding: 0;\n}\n\n.p-10 {\n padding: 10px;\n}\n\n.fb-300px {\n flex-basis: 400px;\n}\n\n.flex-row {\n display: flex;\n flex-direction: row;\n}\n\n.flex-grow-0 {\n flex-grow: 0;\n}\n\n.panels {\n display: flex;\n flex-wrap: wrap;\n flex-direction: row;\n}\n.panels .panel {\n margin: 10px;\n padding: 0;\n flex: 1 0 auto;\n flex-basis: 30%;\n max-width: 50%;\n}\n\n.panel {\n color: #141414;\n flex: 1;\n margin: 10px;\n padding: 0;\n border-radius: 3px;\n flex-direction: column;\n flex-basis: 50%;\n min-width: 400px;\n display: flex;\n}\n.panel > * {\n display: block;\n box-sizing: border-box;\n}\n.panel.full {\n flex: 1 1 100%;\n}\n.panel > h1, .panel > h2, .panel > h3 {\n color: white;\n text-shadow: 2px 4px 3px rgba(0, 0, 0, 0.3);\n}\n.panel.fill, .panel .fill {\n background-color: #f9f9f9;\n box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px;\n border-radius: 3px;\n flex: 1;\n padding: 15px;\n display: block;\n}\n.panel.fill h3, .panel .fill h3 {\n color: #393938;\n}\n.panel > .table {\n background-color: #f9f9f9;\n padding: 15px;\n}\n.panel .panel-header {\n font-size: 24px;\n padding: 10px;\n width: 100%;\n}\n.panel .panel-body {\n padding: 10px;\n flex-grow: 1;\n width: 100%;\n}\n.panel .panel-footer {\n padding: 10px;\n width: 100%;\n}\n\n.col .panel {\n margin-left: 0;\n margin-right: 0;\n}\n\n.f-grow {\n flex-grow: 1;\n}\n\n.facts th {\n font-weight: 500;\n text-align: right;\n min-width: 150px;\n}\n\n.dropdown {\n position: relative;\n display: inline-block;\n}\n.dropdown .content {\n display: none;\n position: absolute;\n z-index: 2;\n background-color: #fff;\n padding: 10px;\n border: 1px solid #333;\n box-shadow: rgba(0, 0, 0, 0.25) 0px 54px 55px, rgba(0, 0, 0, 0.12) 0px -12px 30px, rgba(0, 0, 0, 0.12) 0px 4px 6px, rgba(0, 0, 0, 0.17) 0px 12px 13px, rgba(0, 0, 0, 0.09) 0px -3px 5px;\n}\n.dropdown .content.show {\n display: block;\n}\n\n.pointer {\n cursor: pointer;\n}\n\n.menu {\n visibility: hidden;\n position: fixed;\n background-color: teal;\n word-wrap: unset;\n margin: 0;\n transition: 0.5s;\n opacity: 0;\n padding: 10px;\n border: red 1px dashed;\n}\n.menu.shown {\n opacity: 1;\n visibility: visible;\n}\n\nh2.active {\n border-bottom: orange 3px solid;\n color: white;\n}\n\n/** Guide CSS */\n.dimScreen {\n display: none;\n position: fixed;\n padding: 0;\n margin: 0;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: rgba(0, 0, 0, 0.5);\n z-index: 10;\n}\n\n#note {\n display: none;\n /*background-color: #FDFF47;*/\n background: #d8eef4;\n color: #000;\n position: absolute;\n z-index: 11;\n padding: 10px;\n min-width: 250px;\n box-shadow: rgba(255, 255, 255, 0.4) 0px 8px 24px;\n border-radius: 5px;\n border: 10px solid;\n border-image-slice: 1;\n border-width: 5px;\n border-image-source: linear-gradient(to left, #59c1d5, #abdbe7);\n}\n#note > div {\n margin-bottom: 20px;\n}\n#note button {\n cursor: pointer;\n font-size: 0.8em;\n float: right;\n background: #59c1d5;\n border-radius: 2px;\n border: 5px solid;\n border-image-slice: 1;\n border-width: 2px;\n border-image-source: linear-gradient(to left, #59c1d5, #abdbe7);\n}\n#note h3 {\n margin-top: 5px;\n}\n\n.guide-highlight {\n position: relative;\n background-color: #fff !important;\n z-index: 11;\n box-shadow: rgba(255, 255, 255, 0.4) 0px 8px 24px;\n border-radius: 2px;\n padding: 4px;\n color: #59c1d5 !important;\n}\n.guide-highlight i {\n background-color: #fff !important;\n color: #59c1d5 !important;\n}","@import \"coderr-variables.scss\";\r\n\r\n.card {\r\n input[type=\"radio\"] {\r\n cursor: pointer;\r\n -webkit-appearance: none;\r\n -moz-appearance: none;\r\n appearance: none;\r\n outline: 0;\r\n background: $gray-300;\r\n height: 16px;\r\n width: 16px;\r\n border: 1px solid white;\r\n }\r\n\r\n input[type=\"radio\"]:checked {\r\n background: $blue;\r\n color: $blue !important;\r\n }\r\n\r\n input[type=\"radio\"]:hover {\r\n filter: brightness(90%);\r\n }\r\n\r\n input[type=\"radio\"]:disabled {\r\n background: $gray-700;\r\n opacity: 0.6;\r\n pointer-events: none;\r\n }\r\n\r\n input[type=\"radio\"]:after {\r\n content: '';\r\n position: relative;\r\n left: 40%;\r\n top: 20%;\r\n width: 15%;\r\n height: 40%;\r\n display: none;\r\n }\r\n\r\n input[type=\"radio\"]:checked:after {\r\n display: block;\r\n }\r\n\r\n input[type=\"radio\"]:disabled:after {\r\n border-color: $gray-100;\r\n }\r\n}\r\n","$gray-100: #f4f4f4;\r\n$gray-200: #ededed;\r\n$gray-300: #eee;\r\n$gray-400: #dddddd;\r\n$gray-500: #706f6f;\r\n$gray-600: #3c3c3b;\r\n$gray-700: #393938;\r\n$gray-800: #2e2d2c;\r\n$gray-900: #141414;\r\n\r\n\r\n$blue: #59c1d5;\r\n$blue-60: #abdbe7;\r\n$blue-30: #d8eef4;\r\n\r\n$red: #f18c65;\r\n$red-80: #f3a383;\r\n$red-60: #fac981;\r\n$red-30: #feede5;\r\n\r\n$base-font-size: 16px;\r\n\r\n// Theme light\r\n\r\n$panel-bg: #f9f9f9;\r\n$panel-text: $gray-900;\r\n\r\n\r\n$text-primary: #fff;\r\n$text-accent-1: $blue;\r\n$text-accent-2: $red;\r\n\r\n$body-background: $blue;\r\n\r\n$bg-primary: $blue;\r\n$bg-secondary: #fff;\r\n$bg-accent: $red;\r\n\r\n$input-bg: $blue-60;\r\n$input-border-color: darken($blue-60, 50%);\r\n\r\n$nav-bg: $gray-900;\r\n$nav-text: $gray-400;\r\n$nav-sub-bg: $gray-800;\r\n$nav-sub-tab: darken($blue, 30%);\r\n\r\n\r\n$light: $gray-200;\r\n$dark: $gray-700;\r\n\r\n$font-family-sans-serif: \"SofiaProRegular\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\" !default;\r\n","@import \"../_partials/coderr-variables.scss\";\r\n\r\n.bg {\r\n &.white {\r\n background-color: white;\r\n }\r\n}\r\n\r\n.alert {\r\n padding: 10px;\r\n\r\n &.warning {\r\n background-color: $red;\r\n border: 1px solid darken($red, 10);\r\n color: $light;\r\n }\r\n\r\n &.success {\r\n background-color: $blue-30;\r\n border: 1px solid darken($blue-30, 10);\r\n }\r\n}\r\n","@import \"coderr-variables.scss\";\r\n\r\n.card {\r\n input[type=\"checkbox\"] {\r\n cursor: pointer;\r\n -webkit-appearance: none;\r\n -moz-appearance: none;\r\n appearance: none;\r\n outline: 0;\r\n background: $gray-300;\r\n height: 16px;\r\n width: 16px;\r\n border: 1px solid white;\r\n }\r\n\r\n input[type=\"checkbox\"]:checked {\r\n background: $blue;\r\n color: $blue !important;\r\n }\r\n\r\n input[type=\"checkbox\"]:hover {\r\n filter: brightness(90%);\r\n }\r\n\r\n input[type=\"checkbox\"]:disabled {\r\n background: $gray-700;\r\n opacity: 0.6;\r\n pointer-events: none;\r\n }\r\n\r\n input[type=\"checkbox\"]:after {\r\n content: '';\r\n position: relative;\r\n left: 40%;\r\n top: 20%;\r\n width: 15%;\r\n height: 40%;\r\n display: none;\r\n }\r\n\r\n input[type=\"checkbox\"]:checked:after {\r\n display: block;\r\n }\r\n\r\n input[type=\"checkbox\"]:disabled:after {\r\n border-color: $gray-100;\r\n }\r\n}\r\n","@import \"coderr-variables.scss\";\r\n\r\ninput:invalid {\r\n box-shadow: 0 0 5px 1px $red;\r\n}\r\n\r\ninput:focus:invalid {\r\n box-shadow: none;\r\n}\r\n\r\ninput, select, textarea {\r\n padding: 4px;\r\n border: 1px solid #404040;\r\n border-radius: 3px;\r\n}\r\n\r\n.form {\r\n .form-group {\r\n margin-top: 15px;\r\n }\r\n\r\n label {\r\n display: block;\r\n font-weight: bold;\r\n\r\n &.inline {\r\n display: inline;\r\n }\r\n }\r\n\r\n input {\r\n &.inline {\r\n display: inline;\r\n }\r\n }\r\n\r\n input[type=\"text\"],\r\n input[type=\"number\"],\r\n input[type=\"email\"],\r\n input[type=\"password\"],\r\n textarea,\r\n select {\r\n background-color: $input-bg;\r\n border: 1px solid $input-border-color;\r\n border-radius: 3px;\r\n padding: 8px;\r\n border: darken($input-bg, 20%);\r\n width: 100%;\r\n }\r\n\r\n textarea {\r\n min-height: 100px;\r\n width: 100%;\r\n }\r\n\r\n button {\r\n display: inline-flex;\r\n }\r\n\r\n button[type=\"submit\"], a.submit {\r\n @extend .btn;\r\n background-color: $bg-primary;\r\n border: darken($bg-primary, 20%);\r\n color: $light;\r\n width: inherit;\r\n }\r\n\r\n button[type=\"reset\"], a.reset {\r\n @extend .btn;\r\n color: $light;\r\n background-color: darken($bg-primary, 10%);\r\n border: darken($bg-accent, 20%);\r\n }\r\n}\r\n","\r\n.row {\r\n display: flex;\r\n width: 100%;\r\n flex-flow: row wrap;\r\n max-width: 100%;\r\n}\r\n\r\n.container {\r\n display: block;\r\n max-width: 1400px;\r\n margin: 0 auto;\r\n padding: 30px;\r\n\r\n h1 {\r\n font-size: $base-font-size*3;\r\n }\r\n\r\n padding: -10px;\r\n}\r\n\r\n.col {\r\n justify-content: center;\r\n align-items: start;\r\n flex-basis: 0;\r\n /*flex-direction: column;\r\n flex-basis: auto;*/\r\n flex: 1 1 200px;\r\n margin: 10px;\r\n\r\n /* pre > code wont wrap otherwise */\r\n min-width: 0;\r\n}\r\n\r\n.col-2 {\r\n flex: 2;\r\n}\r\n\r\n.col-3 {\r\n flex: 3;\r\n}\r\n\r\n.hidden {\r\n display: none;\r\n opacity: 0;\r\n}\r\n\r\n.w-100 {\r\n width: 100%;\r\n}\r\n\r\n@media only screen and (max-width: 600px) {\r\n .col {\r\n display: block;\r\n width: 100%;\r\n }\r\n}\r\n\r\n.y-center {\r\n align-items: center;\r\n}\r\n\r\n.x-center {\r\n justify-content: center;\r\n}\r\n\r\n.mx-auto {\r\n margin: 0 auto;\r\n}\r\n\r\n@for $i from 0 through 5 {\r\n .mt-#{$i} {\r\n margin-top: $i*5px;\r\n }\r\n\r\n .mb-#{$i} {\r\n margin-bottom: $i*5px;\r\n }\r\n\r\n .ml-#{$i} {\r\n margin-left: $i*5px !important;\r\n }\r\n\r\n .m-#{$i} {\r\n margin: $i*5px;\r\n }\r\n\r\n .pt-#{$i} {\r\n padding-top: $i*5px;\r\n }\r\n\r\n .pl-#{$i} {\r\n padding-left: $i*5px;\r\n }\r\n\r\n\r\n .pr-#{$i} {\r\n padding-right: $i*5px;\r\n }\r\n\r\n\r\n .pb-#{$i} {\r\n padding-bottom: $i*5px;\r\n }\r\n\r\n .p-#{$i} {\r\n padding: $i*5px;\r\n }\r\n}\r\n","@import \"coderr-variables.scss\";\r\n$btn-color: white !default;\r\n\r\n.panel {\r\n .btn {\r\n background-color: $light;\r\n color: $dark;\r\n display: inline-flex;\r\n }\r\n\r\n &.fill .btn, .fill .btn {\r\n background-color: $blue;\r\n border-color: $blue;\r\n color: $light;\r\n display: inline-flex;\r\n }\r\n\r\n button[type=\"submit\"], a.submit {\r\n @extend .btn;\r\n background-color: $light;\r\n border-color: $light;\r\n border: darken($light, 10%);\r\n color: $dark;\r\n width: inherit;\r\n }\r\n\r\n button[type=\"reset\"], a.reset {\r\n @extend .btn;\r\n color: $light;\r\n background-color: $bg-accent;\r\n border-color: $bg-accent;\r\n border: darken($bg-accent, 10%);\r\n }\r\n}\r\n\r\n\r\na.btn,\r\nbutton.btn,\r\ninput.button {\r\n padding: 10px 12px;\r\n margin-top: 15px;\r\n margin-right: 5px;\r\n border-radius: 3px;\r\n text-decoration: none;\r\n box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);\r\n text-shadow: 1px 1px rgba(0, 0, 0, 0.05);\r\n text-align: center;\r\n cursor: pointer;\r\n display: inline-block;\r\n\r\n &.small {\r\n margin-top: 5px;\r\n margin-right: 2px;\r\n padding: 4px 6px;\r\n font-size: 0.9em;\r\n }\r\n\r\n &.default {\r\n background: $btn-color;\r\n color: $dark;\r\n }\r\n\r\n &.block {\r\n display: block;\r\n width: 100%;\r\n }\r\n\r\n &.light {\r\n background-color: $light;\r\n color: $dark;\r\n }\r\n\r\n &.dark {\r\n background-color: $dark;\r\n color: $light;\r\n }\r\n\r\n &.red {\r\n background-color: $red !important;\r\n color: white;\r\n border: 1px solid darken($red, 1%);\r\n transition: 0.5s;\r\n }\r\n\r\n &.red:hover {\r\n background-color: darken($red, 15) !important;\r\n }\r\n\r\n &.blue, &.default {\r\n background-color: $blue !important;\r\n color: white;\r\n border: 1px solid darken($blue, 1%);\r\n }\r\n\r\n &.red50 {\r\n background-color: rgba($red, 0.75) !important;\r\n color: white;\r\n border: 1px solid rgba($red, 0.95);\r\n }\r\n\r\n &.red-2 {\r\n background-color: lighten($red, 5) !important;\r\n color: white;\r\n border: 1px solid rgba($red, 0.5);\r\n }\r\n\r\n &.white {\r\n background-color: white !important;\r\n color: $gray-800;\r\n border: 1px solid darken(white, 1%);\r\n }\r\n\r\n &.gray {\r\n background-color: $gray-300 !important;\r\n color: $gray-900;\r\n border: 1px solid darken(white, 1%);\r\n\r\n .btn {\r\n background: $blue;\r\n }\r\n }\r\n\r\n &.dark {\r\n background-color: $gray-800 !important;\r\n color: $light;\r\n }\r\n}\r\n","@import \"coderr-variables.scss\";\r\n\r\nheader {\r\n background: $nav-bg;\r\n margin: 0;\r\n padding: 5px;\r\n /*.main {\r\n box-shadow: 0 15px 5px lighten($nav-bg, 20%);\r\n }\r\n*/\r\n img {\r\n vertical-align: middle;\r\n height: 25px;\r\n margin-top: -5px;\r\n }\r\n\r\n ul {\r\n list-style-type: none;\r\n display: inline-block;\r\n\r\n li {\r\n color: $nav-text;\r\n display: inline-block;\r\n margin-left: 20px;\r\n\r\n a {\r\n text-decoration: none;\r\n color: $nav-text;\r\n }\r\n\r\n a:hover {\r\n text-shadow: 0px 0px 2px rgba(255, 255, 255, 0.1), 0px 2px 3px rgba(255, 255, 255, 0.5), 0px 6px 6px rgba(255, 255, 255, 0.2);\r\n color: $text-accent-1;\r\n }\r\n }\r\n }\r\n\r\n .box-shadow {\r\n box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.05);\r\n }\r\n\r\n .left-menu {\r\n flex-grow: 1;\r\n align-self: center;\r\n padding: 5px;\r\n color: #999;\r\n\r\n a {\r\n padding-left: 5px;\r\n padding-right: 5px;\r\n color: white;\r\n text-decoration: none;\r\n font-size: 14px;\r\n }\r\n }\r\n\r\n .right-menu {\r\n align-self: center;\r\n }\r\n\r\n .submenu {\r\n background: $nav-sub-bg;\r\n padding-top: 10px;\r\n padding-left: 60px;\r\n\r\n .groups {\r\n margin-top: 10px;\r\n margin-bottom: 10px;\r\n\r\n a {\r\n background-color: $nav-sub-tab;\r\n padding: 5px;\r\n border-top-left-radius: 5px;\r\n border-top-right-radius: 5px;\r\n }\r\n }\r\n\r\n a {\r\n color: $nav-text;\r\n text-decoration: none;\r\n }\r\n\r\n .application-list {\r\n display: flex;\r\n flex-direction: column;\r\n }\r\n\r\n .application-list div {\r\n padding: 5px;\r\n }\r\n }\r\n}\r\n",".svg {\r\n &.blue {\r\n filter: invert(77%) sepia(17%) saturate(1201%) hue-rotate(144deg) brightness(89%) contrast(87%);\r\n }\r\n\r\n &.red {\r\n filter: invert(57%) sepia(89%) saturate(365%) hue-rotate(326deg) brightness(98%) contrast(92%);\r\n }\r\n}","@import \"_mixins.scss\";\r\n@import \"coderr-variables.scss\";\r\n\r\nspan.muted {\r\n color: $gray-500;\r\n}\r\nspan.small {\r\n font-size: 0.9em;\r\n}\r\n\r\na\r\n{\r\n color: $red;\r\n text-decoration: none;\r\n}\r\n\r\np {\r\n margin-top: 5px;\r\n margin-bottom: 15px;\r\n}\r\n\r\n.text-shadow-1 {\r\n @include text-shadow-1\r\n}\r\n\r\nh2, h3, h4 {\r\n font-weight: 600;\r\n margin-top: 10px;\r\n margin-bottom: 10px;\r\n}\r\n\r\n.text-center {\r\n text-align: center;\r\n}\r\n\r\n.text-right{\r\n text-align: right;\r\n}\r\n.text-left {\r\n text-align: left;\r\n}\r\n\r\n.text-blue {\r\n color: $blue;\r\n}\r\n\r\n.text-dark {\r\n color: $dark;\r\n}\r\n\r\n.text-light {\r\n color: $light;\r\n}\r\n\r\n.text-red {\r\n color: $red;\r\n}\r\n\r\n.text-white {\r\n color: #fff;\r\n}\r\n\r\n.text-muted {\r\n color: #bbb;\r\n}\r\n","@mixin box-shadow-1 {\r\n box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px;\r\n}\r\n\r\n@mixin text-shadow-1 {\r\n text-shadow: 2px 4px 3px rgba(0,0,0, 0.3);\r\n}\r\n","@import \"coderr-variables.scss\";\r\n\r\n.table {\r\n table-layout: fixed;\r\n display: table;\r\n\r\n thead tr {\r\n background-color: $bg-secondary;\r\n\r\n th {\r\n text-align: left;\r\n padding-top: 5px;\r\n padding-bottom: 5px;\r\n }\r\n }\r\n\r\n &.striped tbody {\r\n tr:nth-child(even) {\r\n background-color: lighten($bg-primary, 25%);\r\n }\r\n }\r\n\r\n tbody tr td {\r\n\r\n padding-top: 5px;\r\n padding-bottom: 5px;\r\n }\r\n}\r\n","/* based on angular-toastr css https://github.com/Foxandxss/angular-toastr/blob/cb508fe6801d6b288d3afc525bb40fee1b101650/dist/angular-toastr.css */\n\n/* position */\n.toast-center-center {\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}\n.toast-top-center {\n top: 0;\n right: 0;\n width: 100%;\n}\n.toast-bottom-center {\n bottom: 0;\n right: 0;\n width: 100%;\n}\n.toast-top-full-width {\n top: 0;\n right: 0;\n width: 100%;\n}\n.toast-bottom-full-width {\n bottom: 0;\n right: 0;\n width: 100%;\n}\n.toast-top-left {\n top: 12px;\n left: 12px;\n}\n.toast-top-right {\n top: 12px;\n right: 12px;\n}\n.toast-bottom-right {\n right: 12px;\n bottom: 12px;\n}\n.toast-bottom-left {\n bottom: 12px;\n left: 12px;\n}\n\n/* toast styles */\n.toast-title {\n font-weight: bold;\n}\n.toast-message {\n word-wrap: break-word;\n}\n.toast-message a,\n.toast-message label {\n color: #FFFFFF;\n}\n.toast-message a:hover {\n color: #CCCCCC;\n text-decoration: none;\n}\n.toast-close-button {\n position: relative;\n right: -0.3em;\n top: -0.3em;\n float: right;\n font-size: 20px;\n font-weight: bold;\n color: #FFFFFF;\n text-shadow: 0 1px 0 #ffffff;\n /* opacity: 0.8; */\n}\n.toast-close-button:hover,\n.toast-close-button:focus {\n color: #000000;\n text-decoration: none;\n cursor: pointer;\n opacity: 0.4;\n}\n/*Additional properties for button version\n iOS requires the button element instead of an anchor tag.\n If you want the anchor version, it requires `href=\"#\"`.*/\nbutton.toast-close-button {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n}\n.toast-container {\n pointer-events: none;\n position: fixed;\n z-index: 999999;\n}\n.toast-container * {\n box-sizing: border-box;\n}\n.toast-container .ngx-toastr {\n position: relative;\n overflow: hidden;\n margin: 0 0 6px;\n padding: 15px 15px 15px 50px;\n width: 300px;\n border-radius: 3px 3px 3px 3px;\n background-position: 15px center;\n background-repeat: no-repeat;\n background-size: 24px;\n box-shadow: 0 0 12px #999999;\n color: #FFFFFF;\n}\n.toast-container .ngx-toastr:hover {\n box-shadow: 0 0 12px #000000;\n opacity: 1;\n cursor: pointer;\n}\n/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/info-circle.svg */\n.toast-info {\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOS4wNDMgOCA4IDExOS4wODMgOCAyNTZjMCAxMzYuOTk3IDExMS4wNDMgMjQ4IDI0OCAyNDhzMjQ4LTExMS4wMDMgMjQ4LTI0OEM1MDQgMTE5LjA4MyAzOTIuOTU3IDggMjU2IDh6bTAgMTEwYzIzLjE5NiAwIDQyIDE4LjgwNCA0MiA0MnMtMTguODA0IDQyLTQyIDQyLTQyLTE4LjgwNC00Mi00MiAxOC44MDQtNDIgNDItNDJ6bTU2IDI1NGMwIDYuNjI3LTUuMzczIDEyLTEyIDEyaC04OGMtNi42MjcgMC0xMi01LjM3My0xMi0xMnYtMjRjMC02LjYyNyA1LjM3My0xMiAxMi0xMmgxMnYtNjRoLTEyYy02LjYyNyAwLTEyLTUuMzczLTEyLTEydi0yNGMwLTYuNjI3IDUuMzczLTEyIDEyLTEyaDY0YzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MTAwaDEyYzYuNjI3IDAgMTIgNS4zNzMgMTIgMTJ2MjR6Jy8+PC9zdmc+\");\n}\n/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/times-circle.svg */\n.toast-error {\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTI1NiA4QzExOSA4IDggMTE5IDggMjU2czExMSAyNDggMjQ4IDI0OCAyNDgtMTExIDI0OC0yNDhTMzkzIDggMjU2IDh6bTEyMS42IDMxMy4xYzQuNyA0LjcgNC43IDEyLjMgMCAxN0wzMzggMzc3LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwyNTYgMzEybC02NS4xIDY1LjZjLTQuNyA0LjctMTIuMyA0LjctMTcgMEwxMzQuNCAzMzhjLTQuNy00LjctNC43LTEyLjMgMC0xN2w2NS42LTY1LTY1LjYtNjUuMWMtNC43LTQuNy00LjctMTIuMyAwLTE3bDM5LjYtMzkuNmM0LjctNC43IDEyLjMtNC43IDE3IDBsNjUgNjUuNyA2NS4xLTY1LjZjNC43LTQuNyAxMi4zLTQuNyAxNyAwbDM5LjYgMzkuNmM0LjcgNC43IDQuNyAxMi4zIDAgMTdMMzEyIDI1Nmw2NS42IDY1LjF6Jy8+PC9zdmc+\");\n}\n/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/check.svg */\n.toast-success {\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1MTIgNTEyJyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTE3My44OTggNDM5LjQwNGwtMTY2LjQtMTY2LjRjLTkuOTk3LTkuOTk3LTkuOTk3LTI2LjIwNiAwLTM2LjIwNGwzNi4yMDMtMzYuMjA0YzkuOTk3LTkuOTk4IDI2LjIwNy05Ljk5OCAzNi4yMDQgMEwxOTIgMzEyLjY5IDQzMi4wOTUgNzIuNTk2YzkuOTk3LTkuOTk3IDI2LjIwNy05Ljk5NyAzNi4yMDQgMGwzNi4yMDMgMzYuMjA0YzkuOTk3IDkuOTk3IDkuOTk3IDI2LjIwNiAwIDM2LjIwNGwtMjk0LjQgMjk0LjQwMWMtOS45OTggOS45OTctMjYuMjA3IDkuOTk3LTM2LjIwNC0uMDAxeicvPjwvc3ZnPg==\");\n}\n/* https://github.com/FortAwesome/Font-Awesome-Pro/blob/master/advanced-options/raw-svg/regular/exclamation-triangle.svg */\n.toast-warning {\n background-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCA1NzYgNTEyJyB3aWR0aD0nNTc2JyBoZWlnaHQ9JzUxMic+PHBhdGggZmlsbD0ncmdiKDI1NSwyNTUsMjU1KScgZD0nTTU2OS41MTcgNDQwLjAxM0M1ODcuOTc1IDQ3Mi4wMDcgNTY0LjgwNiA1MTIgNTI3Ljk0IDUxMkg0OC4wNTRjLTM2LjkzNyAwLTU5Ljk5OS00MC4wNTUtNDEuNTc3LTcxLjk4N0wyNDYuNDIzIDIzLjk4NWMxOC40NjctMzIuMDA5IDY0LjcyLTMxLjk1MSA4My4xNTQgMGwyMzkuOTQgNDE2LjAyOHpNMjg4IDM1NGMtMjUuNDA1IDAtNDYgMjAuNTk1LTQ2IDQ2czIwLjU5NSA0NiA0NiA0NiA0Ni0yMC41OTUgNDYtNDYtMjAuNTk1LTQ2LTQ2LTQ2em0tNDMuNjczLTE2NS4zNDZsNy40MTggMTM2Yy4zNDcgNi4zNjQgNS42MDkgMTEuMzQ2IDExLjk4MiAxMS4zNDZoNDguNTQ2YzYuMzczIDAgMTEuNjM1LTQuOTgyIDExLjk4Mi0xMS4zNDZsNy40MTgtMTM2Yy4zNzUtNi44NzQtNS4wOTgtMTIuNjU0LTExLjk4Mi0xMi42NTRoLTYzLjM4M2MtNi44ODQgMC0xMi4zNTYgNS43OC0xMS45ODEgMTIuNjU0eicvPjwvc3ZnPg==\");\n}\n.toast-container.toast-top-center .ngx-toastr,\n.toast-container.toast-bottom-center .ngx-toastr {\n width: 300px;\n margin-left: auto;\n margin-right: auto;\n}\n.toast-container.toast-top-full-width .ngx-toastr,\n.toast-container.toast-bottom-full-width .ngx-toastr {\n width: 96%;\n margin-left: auto;\n margin-right: auto;\n}\n.ngx-toastr {\n background-color: #030303;\n pointer-events: auto;\n}\n.toast-success {\n background-color: #51A351;\n}\n.toast-error {\n background-color: #BD362F;\n}\n.toast-info {\n background-color: #2F96B4;\n}\n.toast-warning {\n background-color: #F89406;\n}\n.toast-progress {\n position: absolute;\n left: 0;\n bottom: 0;\n height: 4px;\n background-color: #000000;\n opacity: 0.4;\n}\n/* Responsive Design */\n@media all and (max-width: 240px) {\n .toast-container .ngx-toastr.div {\n padding: 8px 8px 8px 50px;\n width: 11em;\n }\n .toast-container .toast-close-button {\n right: -0.2em;\n top: -0.2em;\n }\n}\n@media all and (min-width: 241px) and (max-width: 480px) {\n .toast-container .ngx-toastr.div {\n padding: 8px 8px 8px 50px;\n width: 18em;\n }\n .toast-container .toast-close-button {\n right: -0.2em;\n top: -0.2em;\n }\n}\n@media all and (min-width: 481px) and (max-width: 768px) {\n .toast-container .ngx-toastr.div {\n padding: 15px 15px 15px 50px;\n width: 25em;\n }\n}\n"],"names":[],"sourceRoot":"webpack:///"} \ No newline at end of file diff --git a/src/Server/Coderr.Server.sln b/src/Server/Coderr.Server.sln new file mode 100644 index 00000000..a7dc7c6d --- /dev/null +++ b/src/Server/Coderr.Server.sln @@ -0,0 +1,181 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32112.339 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.App", "Coderr.Server.App\Coderr.Server.App.csproj", "{5EF42A74-9323-49FA-A1F6-974D6DE77202}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Api", "Coderr.Server.Api\Coderr.Server.Api.csproj", "{FC331A95-FCA4-4764-8004-0884665DD01F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.SqlServer", "Coderr.Server.SqlServer\Coderr.Server.SqlServer.csproj", "{B967BEEA-CDDD-4A83-A4F2-1C946099ED51}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.SqlServer.Tests", "Coderr.Server.SqlServer.Tests\Coderr.Server.SqlServer.Tests.csproj", "{502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.ReportAnalyzer", "Coderr.Server.ReportAnalyzer\Coderr.Server.ReportAnalyzer.csproj", "{29FBF805-CAFD-426A-A576-9756D375BF18}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.App.Tests", "Coderr.Server.App.Tests\Coderr.Server.App.Tests.csproj", "{9031CF08-2778-487B-8E11-2F45714875D1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Infrastructure", "Coderr.Server.Infrastructure\Coderr.Server.Infrastructure.csproj", "{A78A50DA-C9D7-47F2-8528-D7EE39D91924}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Api.Client", "Coderr.Server.Api.Client\Coderr.Server.Api.Client.csproj", "{017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Api.Client.Tests", "Coderr.Server.Api.Client.Tests\Coderr.Server.Api.Client.Tests.csproj", "{62989500-31BC-44FA-97CA-84F484C6F1AA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.ReportAnalyzer.Tests", "Coderr.Server.ReportAnalyzer.Tests\Coderr.Server.ReportAnalyzer.Tests.csproj", "{421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Abstractions", "Coderr.Server.Abstractions\Coderr.Server.Abstractions.csproj", "{47A845A0-FCC2-46F1-A553-0F7110BDDBEA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.ReportAnalyzer.Abstractions", "Coderr.Server.ReportAnalyzer.Abstractions\Coderr.Server.ReportAnalyzer.Abstractions.csproj", "{CA835337-B8F6-447F-8144-F69977363C45}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Domain", "Coderr.Server.Domain\Coderr.Server.Domain.csproj", "{2971156C-327E-4EBD-BDF8-91772451F359}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.WebSite", "Coderr.Server.WebSite\Coderr.Server.WebSite.csproj", "{537FEFB2-76EA-48F1-99D6-BA3DAACED337}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.WebPush", "Coderr.Server.WebPush\Coderr.Server.WebPush.csproj", "{EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Coderr.Server.Infrastructure.Tests", "Coderr.Server.Infrastructure.Tests\Coderr.Server.Infrastructure.Tests.csproj", "{ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Premise|Any CPU = Premise|Any CPU + PremiseDebug|Any CPU = PremiseDebug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {5EF42A74-9323-49FA-A1F6-974D6DE77202}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5EF42A74-9323-49FA-A1F6-974D6DE77202}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5EF42A74-9323-49FA-A1F6-974D6DE77202}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {5EF42A74-9323-49FA-A1F6-974D6DE77202}.Premise|Any CPU.Build.0 = Premise|Any CPU + {5EF42A74-9323-49FA-A1F6-974D6DE77202}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {5EF42A74-9323-49FA-A1F6-974D6DE77202}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {5EF42A74-9323-49FA-A1F6-974D6DE77202}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5EF42A74-9323-49FA-A1F6-974D6DE77202}.Release|Any CPU.Build.0 = Release|Any CPU + {FC331A95-FCA4-4764-8004-0884665DD01F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FC331A95-FCA4-4764-8004-0884665DD01F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FC331A95-FCA4-4764-8004-0884665DD01F}.Premise|Any CPU.ActiveCfg = Release|Any CPU + {FC331A95-FCA4-4764-8004-0884665DD01F}.Premise|Any CPU.Build.0 = Release|Any CPU + {FC331A95-FCA4-4764-8004-0884665DD01F}.PremiseDebug|Any CPU.ActiveCfg = Debug|Any CPU + {FC331A95-FCA4-4764-8004-0884665DD01F}.PremiseDebug|Any CPU.Build.0 = Debug|Any CPU + {FC331A95-FCA4-4764-8004-0884665DD01F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FC331A95-FCA4-4764-8004-0884665DD01F}.Release|Any CPU.Build.0 = Release|Any CPU + {B967BEEA-CDDD-4A83-A4F2-1C946099ED51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B967BEEA-CDDD-4A83-A4F2-1C946099ED51}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B967BEEA-CDDD-4A83-A4F2-1C946099ED51}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {B967BEEA-CDDD-4A83-A4F2-1C946099ED51}.Premise|Any CPU.Build.0 = Premise|Any CPU + {B967BEEA-CDDD-4A83-A4F2-1C946099ED51}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {B967BEEA-CDDD-4A83-A4F2-1C946099ED51}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {B967BEEA-CDDD-4A83-A4F2-1C946099ED51}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B967BEEA-CDDD-4A83-A4F2-1C946099ED51}.Release|Any CPU.Build.0 = Release|Any CPU + {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}.Debug|Any CPU.Build.0 = Debug|Any CPU + {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}.Premise|Any CPU.Build.0 = Premise|Any CPU + {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}.Release|Any CPU.ActiveCfg = Release|Any CPU + {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705}.Release|Any CPU.Build.0 = Release|Any CPU + {29FBF805-CAFD-426A-A576-9756D375BF18}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29FBF805-CAFD-426A-A576-9756D375BF18}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29FBF805-CAFD-426A-A576-9756D375BF18}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {29FBF805-CAFD-426A-A576-9756D375BF18}.Premise|Any CPU.Build.0 = Premise|Any CPU + {29FBF805-CAFD-426A-A576-9756D375BF18}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {29FBF805-CAFD-426A-A576-9756D375BF18}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {29FBF805-CAFD-426A-A576-9756D375BF18}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29FBF805-CAFD-426A-A576-9756D375BF18}.Release|Any CPU.Build.0 = Release|Any CPU + {9031CF08-2778-487B-8E11-2F45714875D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9031CF08-2778-487B-8E11-2F45714875D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9031CF08-2778-487B-8E11-2F45714875D1}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {9031CF08-2778-487B-8E11-2F45714875D1}.Premise|Any CPU.Build.0 = Premise|Any CPU + {9031CF08-2778-487B-8E11-2F45714875D1}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {9031CF08-2778-487B-8E11-2F45714875D1}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {9031CF08-2778-487B-8E11-2F45714875D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9031CF08-2778-487B-8E11-2F45714875D1}.Release|Any CPU.Build.0 = Release|Any CPU + {A78A50DA-C9D7-47F2-8528-D7EE39D91924}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A78A50DA-C9D7-47F2-8528-D7EE39D91924}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A78A50DA-C9D7-47F2-8528-D7EE39D91924}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {A78A50DA-C9D7-47F2-8528-D7EE39D91924}.Premise|Any CPU.Build.0 = Premise|Any CPU + {A78A50DA-C9D7-47F2-8528-D7EE39D91924}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {A78A50DA-C9D7-47F2-8528-D7EE39D91924}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {A78A50DA-C9D7-47F2-8528-D7EE39D91924}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A78A50DA-C9D7-47F2-8528-D7EE39D91924}.Release|Any CPU.Build.0 = Release|Any CPU + {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}.Premise|Any CPU.Build.0 = Premise|Any CPU + {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0}.Release|Any CPU.Build.0 = Release|Any CPU + {62989500-31BC-44FA-97CA-84F484C6F1AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62989500-31BC-44FA-97CA-84F484C6F1AA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62989500-31BC-44FA-97CA-84F484C6F1AA}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {62989500-31BC-44FA-97CA-84F484C6F1AA}.Premise|Any CPU.Build.0 = Premise|Any CPU + {62989500-31BC-44FA-97CA-84F484C6F1AA}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {62989500-31BC-44FA-97CA-84F484C6F1AA}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {62989500-31BC-44FA-97CA-84F484C6F1AA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62989500-31BC-44FA-97CA-84F484C6F1AA}.Release|Any CPU.Build.0 = Release|Any CPU + {421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}.Premise|Any CPU.ActiveCfg = Release|Any CPU + {421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}.Premise|Any CPU.Build.0 = Release|Any CPU + {421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}.PremiseDebug|Any CPU.ActiveCfg = Debug|Any CPU + {421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}.PremiseDebug|Any CPU.Build.0 = Debug|Any CPU + {421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {421C787A-079B-4A88-A6C5-1ACD9A7C6A7E}.Release|Any CPU.Build.0 = Release|Any CPU + {47A845A0-FCC2-46F1-A553-0F7110BDDBEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47A845A0-FCC2-46F1-A553-0F7110BDDBEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47A845A0-FCC2-46F1-A553-0F7110BDDBEA}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {47A845A0-FCC2-46F1-A553-0F7110BDDBEA}.Premise|Any CPU.Build.0 = Premise|Any CPU + {47A845A0-FCC2-46F1-A553-0F7110BDDBEA}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {47A845A0-FCC2-46F1-A553-0F7110BDDBEA}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {47A845A0-FCC2-46F1-A553-0F7110BDDBEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47A845A0-FCC2-46F1-A553-0F7110BDDBEA}.Release|Any CPU.Build.0 = Release|Any CPU + {CA835337-B8F6-447F-8144-F69977363C45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CA835337-B8F6-447F-8144-F69977363C45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CA835337-B8F6-447F-8144-F69977363C45}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {CA835337-B8F6-447F-8144-F69977363C45}.Premise|Any CPU.Build.0 = Premise|Any CPU + {CA835337-B8F6-447F-8144-F69977363C45}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {CA835337-B8F6-447F-8144-F69977363C45}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {CA835337-B8F6-447F-8144-F69977363C45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CA835337-B8F6-447F-8144-F69977363C45}.Release|Any CPU.Build.0 = Release|Any CPU + {2971156C-327E-4EBD-BDF8-91772451F359}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2971156C-327E-4EBD-BDF8-91772451F359}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2971156C-327E-4EBD-BDF8-91772451F359}.Premise|Any CPU.ActiveCfg = Premise|Any CPU + {2971156C-327E-4EBD-BDF8-91772451F359}.Premise|Any CPU.Build.0 = Premise|Any CPU + {2971156C-327E-4EBD-BDF8-91772451F359}.PremiseDebug|Any CPU.ActiveCfg = Premise|Any CPU + {2971156C-327E-4EBD-BDF8-91772451F359}.PremiseDebug|Any CPU.Build.0 = Premise|Any CPU + {2971156C-327E-4EBD-BDF8-91772451F359}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2971156C-327E-4EBD-BDF8-91772451F359}.Release|Any CPU.Build.0 = Release|Any CPU + {537FEFB2-76EA-48F1-99D6-BA3DAACED337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {537FEFB2-76EA-48F1-99D6-BA3DAACED337}.Debug|Any CPU.Build.0 = Debug|Any CPU + {537FEFB2-76EA-48F1-99D6-BA3DAACED337}.Premise|Any CPU.ActiveCfg = Release|Any CPU + {537FEFB2-76EA-48F1-99D6-BA3DAACED337}.Premise|Any CPU.Build.0 = Release|Any CPU + {537FEFB2-76EA-48F1-99D6-BA3DAACED337}.PremiseDebug|Any CPU.ActiveCfg = Debug|Any CPU + {537FEFB2-76EA-48F1-99D6-BA3DAACED337}.PremiseDebug|Any CPU.Build.0 = Debug|Any CPU + {537FEFB2-76EA-48F1-99D6-BA3DAACED337}.Release|Any CPU.ActiveCfg = Release|Any CPU + {537FEFB2-76EA-48F1-99D6-BA3DAACED337}.Release|Any CPU.Build.0 = Release|Any CPU + {EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}.Premise|Any CPU.ActiveCfg = Release|Any CPU + {EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}.Premise|Any CPU.Build.0 = Release|Any CPU + {EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}.PremiseDebug|Any CPU.ActiveCfg = Debug|Any CPU + {EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}.PremiseDebug|Any CPU.Build.0 = Debug|Any CPU + {EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EB78BA49-5596-4B1C-89C6-E2375FDF9BC5}.Release|Any CPU.Build.0 = Release|Any CPU + {ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}.Premise|Any CPU.ActiveCfg = Debug|Any CPU + {ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}.Premise|Any CPU.Build.0 = Debug|Any CPU + {ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}.PremiseDebug|Any CPU.ActiveCfg = Debug|Any CPU + {ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}.PremiseDebug|Any CPU.Build.0 = Debug|Any CPU + {ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACC6BA6C-FA0B-4C5B-A7D2-B00D37EF2BC7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {508BE8C5-ECAB-4876-8E3D-7935C53ACDEB} + EndGlobalSection +EndGlobal diff --git a/src/Server/Coderr.Server.v3.ncrunchsolution b/src/Server/Coderr.Server.v3.ncrunchsolution new file mode 100644 index 00000000..10420ac9 --- /dev/null +++ b/src/Server/Coderr.Server.v3.ncrunchsolution @@ -0,0 +1,6 @@ + + + True + True + + \ No newline at end of file diff --git a/src/Server/Docker/BuildAndPushLinuxImage.bat b/src/Server/Docker/BuildAndPushLinuxImage.bat new file mode 100644 index 00000000..0b4399ae --- /dev/null +++ b/src/Server/Docker/BuildAndPushLinuxImage.bat @@ -0,0 +1,23 @@ +@echo off +COPY docker-compose_linux.yml docker-compose.yml +COPY docker-compose_linux.override.yml docker-compose.override.yml +COPY Dockerfile_Linux ..\Dockerfile + +CD .. + +rem CALL Docker\BuildFrontend.bat Coderr.Server.Web\ + +docker-compose build + +ECHO built successfully + +docker push coderrio/coderr-communityserver + +ECHO successfully pushed to dockerhub + +DEL Dockerfile +CD Docker +DEL docker-compose.yml +DEL docker-compose.override.yml + +ECHO Completed! diff --git a/src/Server/Docker/BuildAndPushWinImage.bat b/src/Server/Docker/BuildAndPushWinImage.bat new file mode 100644 index 00000000..447fc137 --- /dev/null +++ b/src/Server/Docker/BuildAndPushWinImage.bat @@ -0,0 +1,31 @@ +@echo off +SET OLDDIR=%CD% +SET pushMode=%1 +IF "%1"=="" ( + SET pushMode="local" +) +IF "%1"=="remote" ( + SET pushMode="coderrio/communityserver-win" +) + +COPY /y docker-compose_windows.yml ..\docker-compose.yml +COPY /y docker-compose_windows.override.yml ..\docker-compose.override.yml + +cd .. + +COPY /y Coderr.Server.Web\Dockerfile_Windows Coderr.Server.Web\Dockerfile + +rem CALL ..\Docker\BuildFrontend.bat Coderr.Server.Web\ + +docker-compose build +ECHO Built successfully + +docker push %pushMode% +ECHO successfully pushed to %pushMode% + +echo Cleaning up +DEL Coderr.Server.Web\Dockerfile +DEL docker-compose.yml +DEL docker-compose.override.yml +cd %OLDDIR% +ECHO Completed! diff --git a/src/Server/Docker/BuildFrontend.bat b/src/Server/Docker/BuildFrontend.bat new file mode 100644 index 00000000..c2923b15 --- /dev/null +++ b/src/Server/Docker/BuildFrontend.bat @@ -0,0 +1,10 @@ +@echo off +IF "%1"=="" (SET NewDir=..\Coderr.Server.Web) ELSE (SET NewDir="%1") +echo %NewDir% +set OLDDIR=%CD% +CD %NewDir% + +call npm i +call node node_modules/webpack/bin/webpack.js --config webpack.config.prod.js --mode=production + +chdir /d %OLDDIR% diff --git a/src/Server/Docker/docker-compose.override.yml b/src/Server/Docker/docker-compose.override.yml new file mode 100644 index 00000000..e076dd21 --- /dev/null +++ b/src/Server/Docker/docker-compose.override.yml @@ -0,0 +1,16 @@ +version: '3.7' + +services: + coderr.server.web: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=https://+:443;http://+:80 + - ASPNETCORE_HTTPS_PORT=443 + networks: + - coderr_network + ports: + - "60473:80" + - "60474:443" + volumes: + - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro + - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro diff --git a/src/Server/Docker/docker-compose.yml b/src/Server/Docker/docker-compose.yml new file mode 100644 index 00000000..65883027 --- /dev/null +++ b/src/Server/Docker/docker-compose.yml @@ -0,0 +1,18 @@ +version: '3.7' + +services: + coderr.server.web: + image: coderrio/coderrserverweb + build: + context: . + dockerfile: Coderr.Server.Web/Dockerfile + networks: + - coderr_network + +volumes: + esdata: + driver: local + +networks: + coderr_network: + name: coderr_network diff --git a/src/Server/Docker/docker-compose_linux.override.yml b/src/Server/Docker/docker-compose_linux.override.yml new file mode 100644 index 00000000..c54c1c64 --- /dev/null +++ b/src/Server/Docker/docker-compose_linux.override.yml @@ -0,0 +1,15 @@ +version: '3.7' + +services: + coderr.server.web: + environment: + - ASPNETCORE_ENVIRONMENT=Production + - ASPNETCORE_URLS=https://+:443;http://+:80 + - ASPNETCORE_HTTPS_PORT=50473 + networks: + - coderr_network + ports: + - "2500:80" + volumes: + - ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro + - ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro diff --git a/src/Server/Docker/docker-compose_linux.yml b/src/Server/Docker/docker-compose_linux.yml new file mode 100644 index 00000000..65883027 --- /dev/null +++ b/src/Server/Docker/docker-compose_linux.yml @@ -0,0 +1,18 @@ +version: '3.7' + +services: + coderr.server.web: + image: coderrio/coderrserverweb + build: + context: . + dockerfile: Coderr.Server.Web/Dockerfile + networks: + - coderr_network + +volumes: + esdata: + driver: local + +networks: + coderr_network: + name: coderr_network diff --git a/src/Server/Docker/docker-compose_windows.override.yml b/src/Server/Docker/docker-compose_windows.override.yml new file mode 100644 index 00000000..1e5a90c5 --- /dev/null +++ b/src/Server/Docker/docker-compose_windows.override.yml @@ -0,0 +1 @@ +version: '3.7' diff --git a/src/Server/Docker/docker-compose_windows.yml b/src/Server/Docker/docker-compose_windows.yml new file mode 100644 index 00000000..f809afed --- /dev/null +++ b/src/Server/Docker/docker-compose_windows.yml @@ -0,0 +1,25 @@ +version: '3.7' + +services: + coderr.communityserver: + image: coderrio/communityserver-win + build: + context: . + dockerfile: Dockerfile + networks: + - coderr_network_win + #volumes: + # https://github.com/microsoft/DockerTools/issues/24 + #- ${APPDATA}/ASP.NET/Https:C:\Users\ContainerUser\AppData\Roaming\ASP.NET\Https:ro + #- ${APPDATA}/Microsoft/UserSecrets:/.microsoft/usersecrets/ + #- ${APPDATA}/Microsoft/UserSecrets:C:\Users\ContainerAdmin\AppData\Roaming\Microsoft\UserSecrets:ro + #- ${APPDATA}/Microsoft/UserSecrets//var/aspnet-keys:/root/.aspnet/DataProtection-Keys + # $(AppData)/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro" + +volumes: + esdata: + driver: local + +networks: + coderr_network_win: + name: coderr_network_win diff --git a/src/Server/Docker/package-lock.json b/src/Server/Docker/package-lock.json new file mode 100644 index 00000000..48e341a0 --- /dev/null +++ b/src/Server/Docker/package-lock.json @@ -0,0 +1,3 @@ +{ + "lockfileVersion": 1 +} diff --git a/src/Server/Dockerfile b/src/Server/Dockerfile new file mode 100644 index 00000000..6b541937 --- /dev/null +++ b/src/Server/Dockerfile @@ -0,0 +1,28 @@ +# escape=` + +FROM mcr.microsoft.com/powershell:nanoserver-1903 AS downloadnodejs +SHELL ["pwsh", "-Command", "$ErrorActionPreference = 'Stop';$ProgressPreference='silentlyContinue';"] +RUN Invoke-WebRequest -OutFile nodejs.zip -UseBasicParsing "https://nodejs.org/dist/v10.16.3/node-v10.16.3-win-x64.zip"; ` +Expand-Archive nodejs.zip -DestinationPath C:\; ` +Rename-Item "C:\node-v10.16.3-win-x64" c:\nodejs + +FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base +WORKDIR /app +COPY --from=downloadnodejs C:\nodejs\ C:\Windows\system32\ + +FROM microsoft/dotnet:2.2-sdk AS build +COPY --from=downloadnodejs C:\nodejs\ C:\Windows\system32\ +WORKDIR /src +COPY . . +RUN dotnet restore "./Coderr.Server.Web/Coderr.Server.Web.csproj" +WORKDIR "/src/Coderr.Server.Web" +RUN dotnet build "Coderr.Server.Web.csproj" -c Release -o /app + +FROM build AS publish +WORKDIR "/src/Coderr.Server.Web" +RUN dotnet publish "Coderr.Server.Web.csproj" -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=publish /app . +ENTRYPOINT ["dotnet", "Coderr.Server.Web.dll"] diff --git a/src/Server/OneTrueError.Api.Client.Tests/OneTrueError.Api.Client.Tests.csproj b/src/Server/OneTrueError.Api.Client.Tests/OneTrueError.Api.Client.Tests.csproj deleted file mode 100644 index 2a4a9293..00000000 --- a/src/Server/OneTrueError.Api.Client.Tests/OneTrueError.Api.Client.Tests.csproj +++ /dev/null @@ -1,97 +0,0 @@ - - - - - Debug - AnyCPU - {62989500-31BC-44FA-97CA-84F484C6F1AA} - Library - Properties - OneTrueError.Api.Client.Tests - OneTrueError.Api.Client.Tests - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\FluentAssertions.4.14.0\lib\net45\FluentAssertions.dll - True - - - ..\packages\FluentAssertions.4.14.0\lib\net45\FluentAssertions.Core.dll - True - - - - - - - - - - - ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll - True - - - ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll - True - - - ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll - True - - - ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll - True - - - - - - - - - Designer - - - - - {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0} - OneTrueError.Api.Client - - - {fc331a95-fca4-4764-8004-0884665dd01f} - OneTrueError.Api - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client.Tests/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.Api.Client.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 0dd8e240..00000000 --- a/src/Server/OneTrueError.Api.Client.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("OneTrueError.Api.Client.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.Api.Client.Tests")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("62989500-31bc-44fa-97ca-84f484c6f1aa")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client.Tests/TryTheClient.cs b/src/Server/OneTrueError.Api.Client.Tests/TryTheClient.cs deleted file mode 100644 index 2aae228e..00000000 --- a/src/Server/OneTrueError.Api.Client.Tests/TryTheClient.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Net; -using System.Threading.Tasks; -using FluentAssertions; -using OneTrueError.Api.Core.Accounts.Queries; -using Xunit; - -namespace OneTrueError.Api.Client.Tests -{ -#if DEBUG - public class TryTheClient - { - //[Fact] - public async Task Test() - { - var client = new OneTrueApiClient(); - client.Open(new Uri("http://localhost/onetrueerror/"), "", ""); - FindAccountByUserNameResult result = null; - try - { - result = await client.QueryAsync(new FindAccountByUserName("admin")); - } - catch (WebException) - { - } - - - result.Should().NotBeNull(); - } - } -#endif -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client.Tests/packages.config b/src/Server/OneTrueError.Api.Client.Tests/packages.config deleted file mode 100644 index bca10e7e..00000000 --- a/src/Server/OneTrueError.Api.Client.Tests/packages.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client/Json/IncludeNonPublicMembersContractResolver.cs b/src/Server/OneTrueError.Api.Client/Json/IncludeNonPublicMembersContractResolver.cs deleted file mode 100644 index 462e0c1d..00000000 --- a/src/Server/OneTrueError.Api.Client/Json/IncludeNonPublicMembersContractResolver.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace OneTrueError.Api.Client.Json -{ - /// - /// Allows us to serialize properties with private setters. - /// - internal class IncludeNonPublicMembersContractResolver : DefaultContractResolver - { - /// - /// Creates a for the given - /// . - /// - /// The member's parent . - /// The member to create a for. - /// - /// A created for the given - /// . - /// - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - //TODO: Maybe cache - var prop = base.CreateProperty(member, memberSerialization); - - if (!prop.Writable) - { - var property = member as PropertyInfo; - if (property != null) - { - var hasPrivateSetter = property.GetSetMethod(true) != null; - prop.Writable = hasPrivateSetter; - } - } - - return prop; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client/OneTrueClient.cs b/src/Server/OneTrueError.Api.Client/OneTrueClient.cs deleted file mode 100644 index 1cb9abc4..00000000 --- a/src/Server/OneTrueError.Api.Client/OneTrueClient.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Net; -using System.Security.Cryptography; -using System.Text; -using System.Threading.Tasks; -using DotNetCqs; -using Newtonsoft.Json; -using OneTrueError.Api.Client.Json; - -namespace OneTrueError.Api.Client -{ - /// - /// Client for the OneTrueError server API - /// - public class OneTrueApiClient : IQueryBus, ICommandBus, IEventBus - { - private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings - { - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - Formatting = Formatting.Indented - }; - - private string _apiKey; - - private string _sharedSecret; - private Uri _uri; - - - /// - /// Creates a new instance of . - /// - public OneTrueApiClient() - { - _jsonSerializerSettings.ContractResolver = new IncludeNonPublicMembersContractResolver(); - } - - /// - /// Execute a command - /// - /// type of query (from the OneTrueError.Api class library) - /// command to execute - /// task - public async Task ExecuteAsync(T command) where T : Command - { - var response = await RequestAsync("POST", "command", command); - response.Close(); - } - - /// - /// Publish an event - /// - /// type of event (from the OneTrueError.Api class library) - /// event to publish - /// task - public async Task PublishAsync(TApplicationEvent e) - where TApplicationEvent : ApplicationEvent - { - var response = await RequestAsync("POST", "event", e); - response.Close(); - } - - /// - /// Make a query - /// - /// Result from a query (a class from the OneTrueError.Api library) - /// - /// - public async Task QueryAsync(Query query) - { - //TODO: Unwrap the cqs object to query parameters instead - //to allow caching in the server - var response = await RequestAsync("POST", "query", query); - return await DeserializeResponse(response); - } - - /// - /// Open a channel - /// - /// Root URL to the OneTrueError web - /// Api key from the admin area in OneTrueError web - /// Shared secret from the admin area in OneTrueError web - public void Open(Uri uri, string apiKey, string sharedSecret) - { - if (apiKey == null) throw new ArgumentNullException(nameof(apiKey)); - if (sharedSecret == null) throw new ArgumentNullException(nameof(sharedSecret)); - if (uri == null) throw new ArgumentNullException(nameof(uri)); - - _apiKey = apiKey; - _sharedSecret = sharedSecret; - _uri = uri; - } - - private async Task DeserializeResponse(HttpWebResponse response) - { - var responseStream = response.GetResponseStream(); - var jsonBuf = new byte[response.ContentLength]; - await responseStream.ReadAsync(jsonBuf, 0, jsonBuf.Length); - var jsonStr = Encoding.UTF8.GetString(jsonBuf); - var responseObj = JsonConvert.DeserializeObject(jsonStr, typeof(TResult), _jsonSerializerSettings); - return (TResult) responseObj; - } - - private async Task RequestAsync(string httpMethod, string cqsType, object cqsObject) - { - var request = WebRequest.CreateHttp(_uri + "api/cqs"); - request.Method = httpMethod; - request.Headers.Add("X-Api-Key", _apiKey); - request.Headers.Add("X-Cqs-Name", cqsObject.GetType().Name); - - var stream = await request.GetRequestStreamAsync(); - var json = JsonConvert.SerializeObject(cqsObject, _jsonSerializerSettings); - var buffer = Encoding.UTF8.GetBytes(json); - - var hamc = new HMACSHA256(Encoding.UTF8.GetBytes(_sharedSecret.ToLower())); - var hash = hamc.ComputeHash(buffer); - var signature = Convert.ToBase64String(hash); - - await stream.WriteAsync(buffer, 0, buffer.Length); - - request.Headers.Add("X-Api-Signature", signature); - - return (HttpWebResponse) await request.GetResponseAsync(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client/OneTrueError.Api.Client.csproj b/src/Server/OneTrueError.Api.Client/OneTrueError.Api.Client.csproj deleted file mode 100644 index 9c6eb078..00000000 --- a/src/Server/OneTrueError.Api.Client/OneTrueError.Api.Client.csproj +++ /dev/null @@ -1,63 +0,0 @@ - - - - - Debug - AnyCPU - {017F8863-3DE0-4AD2-9ED3-5ACB87BBBCD0} - Library - Properties - OneTrueError.Api.Client - OneTrueError.Api.Client - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\OneTrueError.Api.Client.XML - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.Api.Client/Properties/AssemblyInfo.cs deleted file mode 100644 index 0a6adbdb..00000000 --- a/src/Server/OneTrueError.Api.Client/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("OneTrueError.Api.Client")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.Api.Client")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("017f8863-3de0-4ad2-9ed3-5acb87bbbcd0")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Server/OneTrueError.Api.Client/packages.config b/src/Server/OneTrueError.Api.Client/packages.config deleted file mode 100644 index 71edc4dd..00000000 --- a/src/Server/OneTrueError.Api.Client/packages.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Commands/RegisterAccount.cs b/src/Server/OneTrueError.Api/Core/Accounts/Commands/RegisterAccount.cs deleted file mode 100644 index 6b1071de..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Commands/RegisterAccount.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Commands -{ - /// - /// Register a new account and send out an activation email. - /// - public class RegisterAccount : Command - { - /// - /// Creates a new instance of - /// - /// User name - /// Password as entered by the user - /// Email address - public RegisterAccount(string userName, string password, string email) - { - if (userName == null) throw new ArgumentNullException("userName"); - if (password == null) throw new ArgumentNullException("password"); - if (email == null) throw new ArgumentNullException("email"); - UserName = userName; - Password = password; - Email = email; - } - - /// - /// Serialization constructor. - /// - protected RegisterAccount() - { - } - - /// - /// Email address. - /// - public string Email { get; private set; } - - /// - /// Password as entered by the user. - /// - public string Password { get; private set; } - - /// - /// User name - /// - public string UserName { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Commands/RequestPasswordReset.cs b/src/Server/OneTrueError.Api/Core/Accounts/Commands/RequestPasswordReset.cs deleted file mode 100644 index 67f93bc5..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Commands/RequestPasswordReset.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using DotNetCqs; -using OneTrueError.Api.Core.Accounts.Requests; - -namespace OneTrueError.Api.Core.Accounts.Commands -{ - /// - /// Request a password reset (i.e. lock account, email an activation link to the user and wait for activation). - /// - /// - /// - /// will be exeucted when the user clicks on the link. - /// - /// - public class RequestPasswordReset : Command - { - /// - /// Serialization constructor - /// - protected RequestPasswordReset() - { - } - - /// - /// Create a new instance of . - /// - /// Email address associated with the user account. - public RequestPasswordReset(string emailAddress) - { - if (emailAddress == null) throw new ArgumentNullException("emailAddress"); - EmailAddress = emailAddress; - } - - /// - /// Email address associated with the user account. - /// - public string EmailAddress { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Events/AccountRegistered.cs b/src/Server/OneTrueError.Api/Core/Accounts/Events/AccountRegistered.cs deleted file mode 100644 index 05f8b0d3..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Events/AccountRegistered.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Events -{ - /// - /// An user have registered an account and activated it. - /// - public class AccountRegistered : ApplicationEvent - { - /// - /// Create a new instance of - - /// - /// Account id (primary key). - /// User name as entered by the user. - public AccountRegistered(int accountId, string userName) - { - if (accountId <= 0) throw new ArgumentNullException("accountId"); - if (userName == null) throw new ArgumentNullException(nameof(userName)); - AccountId = accountId; - UserName = userName; - } - - /// - /// Serialization constructor - /// - protected AccountRegistered() - { - } - - /// - /// Account id (primary key). - /// - public int AccountId { get; private set; } - - /// - /// User name as entered by the user. - /// - public string UserName { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Accounts/NamespaceDoc.cs deleted file mode 100644 index e06f1e7a..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/NamespaceDoc.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Accounts -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Account information (i.e. authentication and authorization) - /// - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/AcceptInvitationReply.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/AcceptInvitationReply.cs deleted file mode 100644 index fbb76b67..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/AcceptInvitationReply.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Reply for . - /// - public class AcceptInvitationReply - { - /// - /// Creates a new instance of . - /// - /// Primary key for the generated account - /// Username - public AcceptInvitationReply(int accountId, string userName) - { - if (userName == null) throw new ArgumentNullException("userName"); - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - - AccountId = accountId; - UserName = userName; - } - - /// - /// Primary key for the generated account - /// - public int AccountId { get; private set; } - - /// - /// Username - /// - public string UserName { get; private set; } - - /// - /// Returns a string that represents the current object. - /// - /// - /// A string that represents the current object. - /// - /// 2 - public override string ToString() - { - return string.Format("{0} [{1}]", UserName, AccountId); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ActivateAccount.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/ActivateAccount.cs deleted file mode 100644 index eb551c7b..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ActivateAccount.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Activate a user account - /// - /// - /// - /// A user have registered and then clicked on the activation link in the email. - /// - /// - public class ActivateAccount : Request - { - /// - /// Create a new instance of - - /// - /// Activation key from the email. - public ActivateAccount(string activationKey) - { - if (activationKey == null) throw new ArgumentNullException(nameof(activationKey)); - ActivationKey = activationKey; - } - - /// - /// Serialization constructor. - /// - protected ActivateAccount() - { - } - - /// - /// Activation key from the email. - /// - public string ActivationKey { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ActivateAccountReply.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/ActivateAccountReply.cs deleted file mode 100644 index f7304a75..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ActivateAccountReply.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Reply for . - /// - public class ActivateAccountReply - { - /// - /// Creates a new instance of . - /// - /// Account identifier - /// Username used when registering the account - /// userName - /// accountId - public ActivateAccountReply(int accountId, string userName) - { - if (userName == null) throw new ArgumentNullException("userName"); - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - - AccountId = accountId; - UserName = userName; - } - - /// - /// Serialization constructor. - /// - protected ActivateAccountReply() - { - } - - - /// - /// Account identifier. - /// - public int AccountId { get; set; } - - /// - /// Username as entered when registering. - /// - public string UserName { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ChangePasswordReply.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/ChangePasswordReply.cs deleted file mode 100644 index a9be7a45..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ChangePasswordReply.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Reply for . - /// - public class ChangePasswordReply - { - /// - /// Change was successful. - /// - public bool Success { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/IgnoreFieldAttribute.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/IgnoreFieldAttribute.cs deleted file mode 100644 index 39475f62..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/IgnoreFieldAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Used to make the typescript compiler ignore certain properties and types. - /// - public class IgnoreFieldAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/Login.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/Login.cs deleted file mode 100644 index 5bb6a9e0..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/Login.cs +++ /dev/null @@ -1,43 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Login user. - /// - /// - /// - /// The user must have registered an account and activated it. - /// - /// - public class Login : Request - { - /// - /// Creates a new instance of - - /// - /// user name - /// password as entered by the user. Can be empty for cookie logins. - public Login(string userName, string password) - { - UserName = userName; - Password = password; - } - - /// - /// Serialization constructor. - /// - protected Login() - { - } - - /// - /// Password may be empty for cookie logins. - /// - public string Password { get; private set; } - - /// - /// Username - /// - public string UserName { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/LoginReply.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/LoginReply.cs deleted file mode 100644 index 2c40b8e5..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/LoginReply.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Reply for . - /// - public class LoginReply - { - /// - /// Account id - /// - public int AccountId { get; set; } - - /// - /// Login result - /// - public LoginResult Result { get; set; } - - /// - /// User name - /// - public string UserName { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/LoginResult.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/LoginResult.cs deleted file mode 100644 index 1f1a427f..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/LoginResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// How the login went - /// - public enum LoginResult - { - /// - /// Account is or became locked - /// - Locked, - - /// - /// Incorrect username or password. - /// - IncorrectLogin, - - /// - /// Yay! - /// - Successful - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ResetPassword.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/ResetPassword.cs deleted file mode 100644 index 54b44338..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ResetPassword.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using DotNetCqs; -using OneTrueError.Api.Core.Accounts.Commands; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Reset password (i.e. the second step after ). - /// - public class ResetPassword : Request - { - /// - /// Create a new instance of . - /// - /// - /// - public ResetPassword(string activationKey, string newPassword) - { - if (activationKey == null) throw new ArgumentNullException("activationKey"); - if (newPassword == null) throw new ArgumentNullException("newPassword"); - ActivationKey = activationKey; - NewPassword = newPassword; - } - - /// - /// Serialization constructor. - /// - protected ResetPassword() - { - } - - /// - /// Activation key, part of the activation email. - /// - public string ActivationKey { get; private set; } - - /// - /// New password as entered by the user. - /// - public string NewPassword { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ResetPasswordReply.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/ResetPasswordReply.cs deleted file mode 100644 index 1e0f7398..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ResetPasswordReply.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Reply for . - /// - public class ResetPasswordReply - { - /// - /// Reset was successful. - /// - public bool Success { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ValidateNewLogin.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/ValidateNewLogin.cs deleted file mode 100644 index b0b984b7..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ValidateNewLogin.cs +++ /dev/null @@ -1,21 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Check if the user name or email address are taken - /// - public class ValidateNewLogin : Request - { - /// - /// Email address - /// - public string Email { get; set; } - - - /// - /// User name - /// - public string UserName { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ValidateNewLoginReply.cs b/src/Server/OneTrueError.Api/Core/Accounts/Requests/ValidateNewLoginReply.cs deleted file mode 100644 index c8b333b2..00000000 --- a/src/Server/OneTrueError.Api/Core/Accounts/Requests/ValidateNewLoginReply.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace OneTrueError.Api.Core.Accounts.Requests -{ - /// - /// Reply for . - /// - public class ValidateNewLoginReply - { - /// - /// The given email address is already associated with an account. - /// - public bool EmailIsTaken { get; set; } - - /// - /// The given username is already associated with an account. - /// - public bool UserNameIsTaken { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Commands/CreateApiKey.cs b/src/Server/OneTrueError.Api/Core/ApiKeys/Commands/CreateApiKey.cs deleted file mode 100644 index e3d8d154..00000000 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Commands/CreateApiKey.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.ApiKeys.Commands -{ - /// - /// Create a new api key - /// - [AuthorizeRoles("SysAdmin")] - public class CreateApiKey : Command - { - /// - /// Creates a new instance of . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public CreateApiKey(string applicationName, string apiKey, string sharedSecret, int[] applicationIds) - { - if (applicationName == null) throw new ArgumentNullException("applicationName"); - if (apiKey == null) throw new ArgumentNullException("apiKey"); - if (sharedSecret == null) throw new ArgumentNullException("sharedSecret"); - if (applicationIds == null) throw new ArgumentNullException("applicationIds"); - - ApplicationName = applicationName; - ApiKey = apiKey; - SharedSecret = sharedSecret; - ApplicationIds = applicationIds; - } - - /// - /// Creates a new instance of . - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public CreateApiKey(string applicationName, string apiKey, string sharedSecret) - { - if (applicationName == null) throw new ArgumentNullException("applicationName"); - if (apiKey == null) throw new ArgumentNullException("apiKey"); - if (sharedSecret == null) throw new ArgumentNullException("sharedSecret"); - - ApplicationName = applicationName; - ApiKey = apiKey; - SharedSecret = sharedSecret; - ApplicationIds = new int[0]; - } - - /// - /// Serialization constructor - /// - protected CreateApiKey() - { - } - - - /// - /// Must always be the one that creates the key (will be assigned by the CommandBus per convention) - /// - public int AccountId { get; set; } - - /// - /// Generated api key - /// - public string ApiKey { get; set; } - - /// - /// applications that this key may modify. Empty = allow for all applications. - /// - public int[] ApplicationIds { get; set; } - - /// - /// Application that uses this api key - /// - public string ApplicationName { get; set; } - - /// - /// Used to sign all requests. - /// - public string SharedSecret { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Events/ApiKeyRemoved.cs b/src/Server/OneTrueError.Api/Core/ApiKeys/Events/ApiKeyRemoved.cs deleted file mode 100644 index 255c88fd..00000000 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Events/ApiKeyRemoved.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace OneTrueError.Api.Core.ApiKeys.Events -{ - internal class ApiKeyRemoved - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeys.cs b/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeys.cs deleted file mode 100644 index b020e2e2..00000000 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/Queries/ListApiKeys.cs +++ /dev/null @@ -1,11 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.ApiKeys.Queries -{ - /// - /// List all created keys - /// - public class ListApiKeys : Query - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ApiKeys/ReadMe.md b/src/Server/OneTrueError.Api/Core/ApiKeys/ReadMe.md deleted file mode 100644 index 9135a120..00000000 --- a/src/Server/OneTrueError.Api/Core/ApiKeys/ReadMe.md +++ /dev/null @@ -1,32 +0,0 @@ -ApiKeys -======== - -Used to allow external applications to talk with OneTrueError. - - -## Example usage - -The following example calls a local OneTrueError server to retreive applications. - -```csharp -var client = new OneTrueApiClient(); -var uri = new Uri("http://yourServer/onetrueerror/"); -client.Open(uri, "theApiKey", "sharedSecret"); -var apps = await client.QueryAsync(new GetApplicationList()); -``` - -Result (serialized as JSON): - -```javascript -[{ - "Id" : 1, - "Name" : "PublicWeb" - }, { - "Id" : 9, - "Name" : "Time reporting system" - }, { - "Id" : 10, - "Name" : "Coffee monitor" - } -] -``` diff --git a/src/Server/OneTrueError.Api/Core/Applications/ApplicationListItem.cs b/src/Server/OneTrueError.Api/Core/Applications/ApplicationListItem.cs deleted file mode 100644 index 83e73ebe..00000000 --- a/src/Server/OneTrueError.Api/Core/Applications/ApplicationListItem.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using OneTrueError.Api.Core.Applications.Queries; - -namespace OneTrueError.Api.Core.Applications -{ - /// - /// Result item for - /// - public class ApplicationListItem - { - /// - /// Creates a new instance of . - /// - /// application identity - /// name of the application - public ApplicationListItem(int id, string name) - { - if (name == null) throw new ArgumentNullException("name"); - if (id <= 0) throw new ArgumentOutOfRangeException("id"); - - Id = id; - Name = name; - } - - /// - /// Serialization constructor - /// - protected ApplicationListItem() - { - } - - /// - /// Id of the application (primary key) - /// - public int Id { get; set; } - - /// - /// Application name as entered by the user. - /// - public string Name { get; set; } - - /// - /// User that requested this list is the admin of the specified application. - /// - public bool IsAdmin { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Commands/CreateApplication.cs b/src/Server/OneTrueError.Api/Core/Applications/Commands/CreateApplication.cs deleted file mode 100644 index 8011617e..00000000 --- a/src/Server/OneTrueError.Api/Core/Applications/Commands/CreateApplication.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Commands -{ - /// - /// Create a new application. - /// - public class CreateApplication : Command - { - /// - /// Creates a new instance of . - /// - /// Name of the application (as entered by the user) - /// Application type - public CreateApplication(string name, TypeOfApplication typeOfApplication) - { - if (name == null) throw new ArgumentNullException("name"); - if (!Enum.IsDefined(typeof(TypeOfApplication), typeOfApplication)) - - throw new ArgumentOutOfRangeException("typeOfApplication"); - Name = name; - TypeOfApplication = typeOfApplication; - } - - /// - /// Generated application key - /// - public string ApplicationKey { get; set; } - - /// - /// User specified name - /// - public string Name { get; set; } - - - /// - /// Application type - /// - public TypeOfApplication TypeOfApplication { get; set; } - - - /// - /// Account id for the user that sent the command - /// - [IgnoreField] - public int UserId { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Events/UserAddedToApplication.cs b/src/Server/OneTrueError.Api/Core/Applications/Events/UserAddedToApplication.cs deleted file mode 100644 index 8cd2f5db..00000000 --- a/src/Server/OneTrueError.Api/Core/Applications/Events/UserAddedToApplication.cs +++ /dev/null @@ -1,41 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Events -{ - namespace OneTrueError.Api.Core.Accounts.Events - { - /// - /// A user have been added directly, or through an invitation - /// - public class UserAddedToApplication : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// Identifier for the application that the user was added to. - /// Account identifier for the user that was added to the application - public UserAddedToApplication(int applicationId, int accountId) - { - ApplicationId = applicationId; - AccountId = accountId; - } - - /// - /// Serialization constructor - /// - protected UserAddedToApplication() - { - } - - /// - /// Account identifier for the user that was added to the application - /// - public int AccountId { get; private set; } - - /// - /// Identifier for the application that the user was added to. - /// - public int ApplicationId { get; private set; } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Applications/NamespaceDoc.cs deleted file mode 100644 index fffd75da..00000000 --- a/src/Server/OneTrueError.Api/Core/Applications/NamespaceDoc.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Applications -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// An application that we can receive exceptions for. - /// - /// - /// - /// It can also be a specific tier in an application. For instance ASP.NET WebApi, or client side (like an Mobile - /// application). - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationInfoResult.cs b/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationInfoResult.cs deleted file mode 100644 index 874eaf29..00000000 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetApplicationInfoResult.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Result for . - /// - public class GetApplicationInfoResult - { - /// - /// App key - /// - public string AppKey { get; set; } - - /// - /// Type of application - /// - public TypeOfApplication ApplicationType { get; set; } - - /// - /// Application id - /// - public int Id { get; set; } - - /// - /// Name of the application. - /// - public string Name { get; set; } - - /// - /// Shared secret, used together with to make sure that the reports come from the correct source. - /// - public string SharedSecret { get; set; } - - /// - /// Total nume rof incidents for this application. - /// - public int TotalIncidentCount { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetOverview.cs b/src/Server/OneTrueError.Api/Core/Applications/Queries/GetOverview.cs deleted file mode 100644 index f2c69022..00000000 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/GetOverview.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Get stats etc that can be presented as an overview for an application. - /// - public class GetApplicationOverview : Query - { - /// - /// Creates a new instance of . - /// - /// - /// applicationId - public GetApplicationOverview(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - } - - /// - /// Serialization constructor - /// - protected GetApplicationOverview() - { - } - - /// - /// Application id to get an overview for. - /// - public int ApplicationId { get; private set; } - - /// - /// Amount of time to look back (i.e. startdate = DateTime.Now.Substract(WindowSize)) - /// - /// - /// 1 = switch to hours - /// - public int NumberOfDays { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/Queries/OverviewStatSummary.cs b/src/Server/OneTrueError.Api/Core/Applications/Queries/OverviewStatSummary.cs deleted file mode 100644 index 6521fe34..00000000 --- a/src/Server/OneTrueError.Api/Core/Applications/Queries/OverviewStatSummary.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace OneTrueError.Api.Core.Applications.Queries -{ - /// - /// Stats for the last seven days - /// - public class OverviewStatSummary - { - /// - /// Number of followers - /// - public int Followers { get; set; } - - /// - /// Number of incidents - /// - public int Incidents { get; set; } - - /// - /// Number of reports received - /// - public int Reports { get; set; } - - /// - /// Number user feedback items - /// - public int UserFeedback { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Applications/TypeOfApplication.cs b/src/Server/OneTrueError.Api/Core/Applications/TypeOfApplication.cs deleted file mode 100644 index 1b0093fc..00000000 --- a/src/Server/OneTrueError.Api/Core/Applications/TypeOfApplication.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace OneTrueError.Api.Core.Applications -{ - /// - /// Kind of application that this is - /// - /// - /// - /// Used to determine how different analytics should be made, like analyzing memory usage (which has to guess the - /// total amount of memory if not included as context information). - /// - /// - /// For instance a OutOfMemoryException isn't as fatal in a mobile application, like it is in a large server - /// application, as the latter is supposed to have large amount of resources. - /// - /// - public enum TypeOfApplication - { - /// - /// Cellphone application - /// - /// - /// - /// An application with limited system resources (memory and usage). - /// - /// - Mobile, - - /// - /// DesktopApplication application (i.e. a windows end user computer) - /// - DesktopApplication, - - /// - /// Server, as a web server or a WCF service. - /// - Server - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Feedback/Events/FeedbackAttachedToIncident.cs b/src/Server/OneTrueError.Api/Core/Feedback/Events/FeedbackAttachedToIncident.cs deleted file mode 100644 index 0883123a..00000000 --- a/src/Server/OneTrueError.Api/Core/Feedback/Events/FeedbackAttachedToIncident.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Feedback.Events -{ - /// - /// Feedback was attached to incident. - /// - public class FeedbackAttachedToIncident : ApplicationEvent - { - /// - /// Incident that the feedback was attached to. - /// - public int IncidentId { get; set; } - - /// - /// Feedback message. - /// - public string Message { get; set; } - - /// - /// Email address to the user that wrote the message (optional) - /// - public string UserEmailAddress { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Feedback/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Feedback/NamespaceDoc.cs deleted file mode 100644 index 12c86ed3..00000000 --- a/src/Server/OneTrueError.Api/Core/Feedback/NamespaceDoc.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Feedback -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Feedback / error description written by the user when the exception was caught. - /// - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/IgnoreFieldAttribute.cs b/src/Server/OneTrueError.Api/Core/IgnoreFieldAttribute.cs deleted file mode 100644 index a02488a5..00000000 --- a/src/Server/OneTrueError.Api/Core/IgnoreFieldAttribute.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace OneTrueError.Api.Core -{ - /// - /// Used to tell the typescript generator to not generate the field. - /// - public class IgnoreFieldAttribute : Attribute - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Events/IncidentReOpened.cs b/src/Server/OneTrueError.Api/Core/Incidents/Events/IncidentReOpened.cs deleted file mode 100644 index e69e8c5d..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/Events/IncidentReOpened.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Incidents.Events -{ - /// - /// Our user had closed the incident and we just got a new report despite that. - /// - public class IncidentReOpened : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// application that the incident belongs to - /// incident id - /// when the new report was created (in the client library) - /// - public IncidentReOpened(int applicationId, int incidentId, DateTime createdAtUtc) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - - ApplicationId = applicationId; - IncidentId = incidentId; - CreatedAtUtc = createdAtUtc; - } - - /// - /// Serialization constructor - /// - protected IncidentReOpened() - { - } - - /// - /// Application that the report belongs to. - /// - public int ApplicationId { get; set; } - - /// - /// when the new report was created (in the client library) - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// Incident that the received report belongs to. - /// - public int IncidentId { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Events/ReportAddedToIncident.cs b/src/Server/OneTrueError.Api/Core/Incidents/Events/ReportAddedToIncident.cs deleted file mode 100644 index 850a5340..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/Events/ReportAddedToIncident.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using DotNetCqs; -using OneTrueError.Api.Core.Reports; - -namespace OneTrueError.Api.Core.Incidents.Events -{ - /// - /// We just received a new report and attached it to the given incident. - /// - public class ReportAddedToIncident : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// incident that the report was added to - /// received report - /// Incident was marked as closed, so the received report opened it again. - /// incident;report - public ReportAddedToIncident(IncidentSummaryDTO incident, ReportDTO report, bool isReOpened) - { - if (incident == null) throw new ArgumentNullException("incident"); - if (report == null) throw new ArgumentNullException("report"); - - Incident = incident; - Report = report; - IsReOpened = isReOpened; - } - - /// - /// Serialization constructor - /// - protected ReportAddedToIncident() - { - } - - /// - /// Incident that the report was added to. - /// - public IncidentSummaryDTO Incident { get; private set; } - - /// - /// Incident was marked as closed, so the received report opened it again. - /// - public bool IsReOpened { get; set; } - - /// - /// Received report. - /// - public ReportDTO Report { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/IncidentOrder.cs b/src/Server/OneTrueError.Api/Core/Incidents/IncidentOrder.cs deleted file mode 100644 index 62717ff7..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/IncidentOrder.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OneTrueError.Api.Core.Incidents -{ - /// - /// How incidents should be ordered in a list - /// - public enum IncidentOrder - { - /// - /// Newest incidents first - /// - Newest, - - /// - /// The incident with the highest number of reports - /// - MostReports, - - /// - /// The incidents with the most given feedback - /// - MostFeedback - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/IncidentSummaryDTO.cs b/src/Server/OneTrueError.Api/Core/Incidents/IncidentSummaryDTO.cs deleted file mode 100644 index 49e70bec..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/IncidentSummaryDTO.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; - -namespace OneTrueError.Api.Core.Incidents -{ - /// - /// A small summary of an incident, typically used to list incidents. - /// - public class IncidentSummaryDTO - { - /// - /// Creates a new instance of . - /// - /// incident id - /// incident name - /// name - /// incident id - public IncidentSummaryDTO(int id, string name) - { - if (name == null) throw new ArgumentNullException("name"); - if (id <= 0) throw new ArgumentOutOfRangeException("id"); - - Id = id; - Name = name; - } - - /// - /// Serialization constructor - /// - protected IncidentSummaryDTO() - { - } - - /// - /// Application that the incident belongs to - /// - public int ApplicationId { get; set; } - - /// - /// Name of that application - /// - public string ApplicationName { get; set; } - - /// - /// When the incident was created (when we received the first report). - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// Incident id - /// - public int Id { get; set; } - - /// - /// Incident was closed but then received a new error report. - /// - public bool IsReOpened { get; set; } - - /// - /// Update is both when the incident was open/closed and when we received a new report. TODO: Should be refactored into - /// two fields. - /// - public DateTime LastUpdateAtUtc { get; set; } - - /// - /// Incident name (typically first line of the exception message) - /// - public string Name { get; set; } - - /// - /// Number of reports that we've received. Should be the total amount (including those that have been deleted due to - /// retention days). - /// - public int ReportCount { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Incidents/NamespaceDoc.cs deleted file mode 100644 index 5504aab4..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/NamespaceDoc.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Incidents -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// All instances of the same exception are grouped together into an incident (i.e. even if the same exception is - /// thrown 100 000 it's still the same incident). - /// - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidentResult.cs b/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidentResult.cs deleted file mode 100644 index cbdfebad..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidentResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; - -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Result for . - /// - public class FindIncidentResult - { - /// - /// Items - /// - public IReadOnlyList Items { get; set; } - - /// - /// Page number (one based index) - /// - public int PageNumber { get; set; } - - /// - /// Items returned for this page - /// - public int PageSize { get; set; } - - /// - /// Total number of items - /// - public int TotalCount { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidentResultItem.cs b/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidentResultItem.cs deleted file mode 100644 index 4ed7d544..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidentResultItem.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; - -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Item for . - /// - public class FindIncidentResultItem - { - /// - /// Creates new instance of . - /// - /// indicent id - /// incident name - public FindIncidentResultItem(int id, string name) - { - if (name == null) throw new ArgumentNullException("name"); - if (id <= 0) throw new ArgumentOutOfRangeException("id"); - Id = id; - Name = name; - } - - /// - /// Serialization constructor - /// - protected FindIncidentResultItem() - { - } - - /// - /// Id of the application that this incident belongs to - /// - public string ApplicationId { get; set; } - - /// - /// Name of the application that this incident belongs to - /// - public string ApplicationName { get; set; } - - /// - /// When the first report was received. - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// Incident id - /// - public int Id { get; private set; } - - - /// - /// Incident have been automatically opened again after being closed by a user. - /// - public bool IsReOpened { get; set; } - - /// - /// When the last report was recieved (or when the last user action was made) - /// - public DateTime LastUpdateAtUtc { get; set; } - - /// - /// Incident name - /// - public string Name { get; private set; } - - /// - /// Total number of received reports (increased even if the number of stored reports are at the limit) - /// - public int ReportCount { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidents.cs b/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidents.cs deleted file mode 100644 index cf9b5f27..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/FindIncidents.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Find incidents - /// - /// - /// - /// Default query is only open incidents with 20 items per page. - /// - /// - public class FindIncidents : Query - { - /// - /// Creates a new instance of . - /// - public FindIncidents() - { - MaxDate = DateTime.MaxValue; - Open = true; - ItemsPerPage = 20; - } - - /// - /// 0 = find for all applications - /// - /// - /// The application identifier. - /// - public int ApplicationId { get; set; } - - /// - /// Include closed incidents - /// - public bool Closed { get; set; } - - /// - /// Include ignored incidents - /// - public bool Ignored { get; set; } - - /// - /// Number of items per page. - /// - public int ItemsPerPage { get; set; } - - /// - /// End of period - /// - public DateTime MaxDate { get; set; } - - /// - /// Start of period - /// - public DateTime MinDate { get; set; } - - /// - /// Include open incidents - /// - public bool Open { get; set; } - - /// - /// Page to fetch (one based index) - /// - public int PageNumber { get; set; } - - /// - /// Include reopened incidents - /// - public bool ReOpened { get; set; } - - /// - /// Will be searched in incident.message and report.stacktrace. - /// - public string FreeText { get; set; } - - /// - /// Sort order - /// - public bool SortAscending { get; set; } - - /// - /// Sort type - /// - public IncidentOrder SortType { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentForClosePage.cs b/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentForClosePage.cs deleted file mode 100644 index 3a850f95..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentForClosePage.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Get incident information tailored for the close page. - /// - public class GetIncidentForClosePage : Query - { - /// - /// Serialization constructor - /// - protected GetIncidentForClosePage() - { - } - - /// - /// Creates a new instance of . - /// - /// incident id - /// incidentId - public GetIncidentForClosePage(int incidentId) - { - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - } - - /// - /// Incident id - /// - public int IncidentId { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentResult.cs b/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentResult.cs deleted file mode 100644 index 25799eee..00000000 --- a/src/Server/OneTrueError.Api/Core/Incidents/Queries/GetIncidentResult.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; - -namespace OneTrueError.Api.Core.Incidents.Queries -{ - /// - /// Keeps track of all occurrences of a single incident (i.e. error reports which generates the same hash code) - /// - public class GetIncidentResult - { - private string _description; - - - /// - /// Application that the incident belongs to - /// - public int ApplicationId { get; private set; } - - /// - /// Context collection names. - /// - public string[] ContextCollections { get; set; } - - /// - /// When the incident was created (when we received the first exception). - /// - public DateTime CreatedAtUtc { get; private set; } - - /// - /// Daily statistics. - /// - public ReportDay[] DayStatistics { get; set; } - - /// - /// Error description (exception message) - /// - public string Description - { - get - { - if (string.IsNullOrEmpty(_description)) - return "Ooops Error!"; - - return _description; - } - set { _description = value; } - } - - /// - /// Number of users that have written what they did when the error occurred. - /// - public int FeedbackCount { get; set; } - - /// - /// Full name of the exception message. - /// - public string FullName { get; private set; } - - /// - /// Used to identify this incident when the hash code is the same as for other incidents. - /// - /// - public string HashCodeIdentifier { get; private set; } - - /// - /// primary key - /// - public int Id { get; private set; } - - /// - /// Ignore future reports for this incident (i.e. no notifications, do not store new reports etc). - /// - /// - /// - /// Report counter will still be updated. - /// - /// - public bool IsIgnored { get; set; } - - /// - /// If the incident was closed and then received error reports again. - /// - public bool IsReOpened { get; set; } - - /// - /// Share solution with the OneTrueError community. - /// - public bool IsSolutionShared { get; set; } - - /// - /// Incident has been marked as solved (i.e. closed) - /// - public bool IsSolved { get; set; } - - /// - /// Solution written last time (if is true). - /// - public DateTime PreviousSolutionAtUtc { get; set; } - - /// - /// Date if is true. - /// - public DateTime ReOpenedAtUtc { get; set; } - - /// - /// Number of received reports. - /// - public int ReportCount { get; set; } - - - /// - /// Generated hash code - /// - public string ReportHashCode { get; private set; } - - /// - /// How the devs solved it. - /// - public string Solution { get; set; } - - /// - /// When the incident was closed/solved. - /// - public DateTime SolvedAtUtc { get; set; } - - /// - /// Stack trace. - /// - public string StackTrace { get; set; } - - /// - /// Identified StackOverflow tags. - /// - public string[] Tags { get; set; } - - /// - /// When the incident was updated (either a new report or changes to the actual incident) - /// - public DateTime UpdatedAtUtc { get; private set; } - - /// - /// Number of users that have supplied their email address - /// - public int WaitingUserCount { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Invitations/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Invitations/NamespaceDoc.cs deleted file mode 100644 index 3f35ab94..00000000 --- a/src/Server/OneTrueError.Api/Core/Invitations/NamespaceDoc.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Invitations -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Invitations are sent on application basis. - /// - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Messaging/Commands/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Messaging/Commands/NamespaceDoc.cs deleted file mode 100644 index 9b44e861..00000000 --- a/src/Server/OneTrueError.Api/Core/Messaging/Commands/NamespaceDoc.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Messaging.Commands -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// User related information (such as name, notifcation settings etc.) - /// - /// - /// While accounts are for login authentication and authorization, users are for information about the individual. - /// - /// - [CompilerGenerated] - class NamespaceDoc - { - } -} diff --git a/src/Server/OneTrueError.Api/Core/Messaging/Commands/SendEmail.cs b/src/Server/OneTrueError.Api/Core/Messaging/Commands/SendEmail.cs deleted file mode 100644 index c9bcb214..00000000 --- a/src/Server/OneTrueError.Api/Core/Messaging/Commands/SendEmail.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Core.Messaging.Commands -{ - /// - /// Send an email. - /// - public class SendEmail : Command - { - /// - /// Create a new instance of . - /// - /// Message to send - public SendEmail(EmailMessage message) - { - if (message == null) throw new ArgumentNullException(nameof(message)); - EmailMessage = message; - } - - /// - /// Serialization constructor - /// - protected SendEmail() - { - } - - /// - /// Message to send - /// - public EmailMessage EmailMessage { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Messaging/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Messaging/NamespaceDoc.cs deleted file mode 100644 index 638f5823..00000000 --- a/src/Server/OneTrueError.Api/Core/Messaging/NamespaceDoc.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Messaging -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Messaging, which includes sending emails. - /// - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/NamespaceDoc.cs deleted file mode 100644 index 73390582..00000000 --- a/src/Server/OneTrueError.Api/Core/NamespaceDoc.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Core - /// - /// - /// Core contains all basic functionality to get OneTrueError running. A minimal set of analysis is done here. - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/ReadMe.md b/src/Server/OneTrueError.Api/Core/ReadMe.md deleted file mode 100644 index 42364f3e..00000000 --- a/src/Server/OneTrueError.Api/Core/ReadMe.md +++ /dev/null @@ -1,4 +0,0 @@ -Core -============ - -Core contains all basic functionality to get OneTrueError running. A minimal set of analysis is done here. diff --git a/src/Server/OneTrueError.Api/Core/Reports/ContextCollectionDTO.cs b/src/Server/OneTrueError.Api/Core/Reports/ContextCollectionDTO.cs deleted file mode 100644 index 80e4306a..00000000 --- a/src/Server/OneTrueError.Api/Core/Reports/ContextCollectionDTO.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace OneTrueError.Api.Core.Reports -{ - /// - /// Context collection DTO. - /// - public class ContextCollectionDTO - { - /// - /// Creates a new instance of . - /// - protected ContextCollectionDTO() - { - } - - /// - /// Creates a new instance of . - /// - /// Name as specified in the client library - /// Properties. - public ContextCollectionDTO(string name, IDictionary items) - { - if (name == null) throw new ArgumentNullException("name"); - if (items == null) throw new ArgumentNullException("items"); - - Name = name; - Properties = items; - } - - - /// - /// Name as specified in the client library - /// - public string Name { get; set; } - - /// - /// Properties. - /// - public IDictionary Properties { get; set; } - - /// - /// Returns a string that represents the current object. - /// - /// - /// A string that represents the current object. - /// - /// 2 - public override string ToString() - { - var flatten = Properties.Select(x => x.Key + "=" + x.Value); - var joinProps = string.Join(", ", flatten); - return string.Format("{0} [{1}]", Name, joinProps); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Reports/NamespaceDoc.cs deleted file mode 100644 index 1652cf95..00000000 --- a/src/Server/OneTrueError.Api/Core/Reports/NamespaceDoc.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Reports -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Reports represent the recieved exception along with all collected context information. - /// - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/ReportDTO.cs b/src/Server/OneTrueError.Api/Core/Reports/ReportDTO.cs deleted file mode 100644 index fc729bc5..00000000 --- a/src/Server/OneTrueError.Api/Core/Reports/ReportDTO.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; - -namespace OneTrueError.Api.Core.Reports -{ - /// - /// Report representation. - /// - public class ReportDTO - { - /// - /// Application that the incident and report belongs in. - /// - public int ApplicationId { get; set; } - - /// - /// A collection of context information such as HTTP request information or computer hardware info. - /// - public ContextCollectionDTO[] ContextCollections { get; set; } - - /// - /// Date specified at client side - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// Exception which was caught. - /// - public ReportExeptionDTO Exception { get; set; } - - /// - /// DB primary key - /// - public int Id { get; set; } - - /// - /// DB primary key - /// - public int IncidentId { get; set; } - - /// - /// Ip of the report uploader. - /// - public string RemoteAddress { get; set; } - - /// - /// Gets error id (unique identifier used in communication with the customer to identify this error) - /// - public string ReportId { get; set; } - - /// - /// Version of the report - /// - public string ReportVersion { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Reports/ReportExeptionDTO.cs b/src/Server/OneTrueError.Api/Core/Reports/ReportExeptionDTO.cs deleted file mode 100644 index 5cff8a49..00000000 --- a/src/Server/OneTrueError.Api/Core/Reports/ReportExeptionDTO.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections.Generic; - -namespace OneTrueError.Api.Core.Reports -{ - /// - /// Model used to wrap all information from an exception. - /// - public class ReportExeptionDTO - { - /// - /// Initializes a new instance of the class. - /// - public ReportExeptionDTO() - { - Properties = new Dictionary(); - } - - /// - /// Assembly name (version included) - /// - public string AssemblyName { get; set; } - - /// - /// Exception base classes. Most specific first: ArgumentOutOfRangeException, ArgumentException, - /// Exception. - /// - public string[] BaseClasses { get; set; } - - /// - /// Everything (exception.ToString()) - /// - public string Everything { get; set; } - - /// - /// Full type name (namespace + class name) - /// - public string FullName { get; set; } - - /// - /// Inner exception (if any; otherwise null). - /// - public ReportExeptionDTO InnerException { get; set; } - - /// - /// Exception message - /// - public string Message { get; set; } - - /// - /// Type name - /// - public string Name { get; set; } - - /// - /// Namespace that the exception is in - /// - public string Namespace { get; set; } - - /// - /// All properties (public and private) - /// - public IDictionary Properties { get; set; } - - /// - /// Stack trace, line numbers included if your app also distributes the PDB files. - /// - public string StackTrace { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Support/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Support/NamespaceDoc.cs deleted file mode 100644 index 8b0e74f2..00000000 --- a/src/Server/OneTrueError.Api/Core/Support/NamespaceDoc.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Support -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Used to get support from Gauffin Interactive AB - /// - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Support/SendSupportRequest.cs b/src/Server/OneTrueError.Api/Core/Support/SendSupportRequest.cs deleted file mode 100644 index cda60f89..00000000 --- a/src/Server/OneTrueError.Api/Core/Support/SendSupportRequest.cs +++ /dev/null @@ -1,25 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Support -{ - /// - /// Send a support request to Gauffin Interactive AB - /// - public class SendSupportRequest : Command - { - /// - /// Problem statement - /// - public string Message { get; set; } - - /// - /// Why do we want support, huh? - /// - public string Subject { get; set; } - - /// - /// Url of the page that did not work - /// - public string Url { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Users/Commands/UpdateNotifications.cs b/src/Server/OneTrueError.Api/Core/Users/Commands/UpdateNotifications.cs deleted file mode 100644 index 65080ecc..00000000 --- a/src/Server/OneTrueError.Api/Core/Users/Commands/UpdateNotifications.cs +++ /dev/null @@ -1,45 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Users.Commands -{ - /// - /// Update user notifications - /// - public class UpdateNotifications : Command - { - /// - /// Application that the settings is for (0 = generel settings) - /// - public int ApplicationId { get; set; } - - /// - /// How to notify when a new incident is created (received an unique exception) - /// - public NotificationState NotifyOnNewIncidents { get; set; } - - /// - /// How to notify when a new report is created (receive an exception) - /// - public NotificationState NotifyOnNewReport { get; set; } - - /// - /// How to notify user when a peak is detected - /// - public NotificationState NotifyOnPeaks { get; set; } - - /// - /// How to notify when we receive a new report on a closed incident. - /// - public NotificationState NotifyOnReOpenedIncident { get; set; } - - /// - /// How to notify when an user have written an error description - /// - public NotificationState NotifyOnUserFeedback { get; set; } - - /// - /// User that configured its settings. - /// - public int UserId { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Users/Commands/UpdatePersonalSettings.cs b/src/Server/OneTrueError.Api/Core/Users/Commands/UpdatePersonalSettings.cs deleted file mode 100644 index 90d7fb02..00000000 --- a/src/Server/OneTrueError.Api/Core/Users/Commands/UpdatePersonalSettings.cs +++ /dev/null @@ -1,32 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Core.Users.Commands -{ - /// - /// Update personal settings. - /// - public class UpdatePersonalSettings : Command - { - /// - /// First name (if specified) - /// - public string FirstName { get; set; } - - - /// - /// Last name (if specified) - /// - public string LastName { get; set; } - - /// - /// Mobile number (E.164 formatted) - /// - public string MobileNumber { get; set; } - - /// - /// Account that the settings are for - /// - [IgnoreField] - public int UserId { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Users/NamespaceDoc.cs b/src/Server/OneTrueError.Api/Core/Users/NamespaceDoc.cs deleted file mode 100644 index d40fd170..00000000 --- a/src/Server/OneTrueError.Api/Core/Users/NamespaceDoc.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api.Core.Users -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// User related information (such as name, notifcation settings etc.) - /// - /// - /// - /// While accounts are for login authentication and authorization, users are for information about the - /// individual. - /// - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Users/NotificationSettings.cs b/src/Server/OneTrueError.Api/Core/Users/NotificationSettings.cs deleted file mode 100644 index 583d8ad4..00000000 --- a/src/Server/OneTrueError.Api/Core/Users/NotificationSettings.cs +++ /dev/null @@ -1,35 +0,0 @@ -using OneTrueError.Api.Core.Users.Queries; - -namespace OneTrueError.Api.Core.Users -{ - /// - /// Notification settings for . - /// - public class NotificationSettings - { - /// - /// How to notify when a new incident is created (received an unique exception) - /// - public NotificationState NotifyOnNewIncidents { get; set; } - - /// - /// How to notify when a new report is created (receive an exception) - /// - public NotificationState NotifyOnNewReport { get; set; } - - /// - /// How to notify user when a peak is detected - /// - public NotificationState NotifyOnPeaks { get; set; } - - /// - /// How to notify when we receive a new report on a closed incident. - /// - public NotificationState NotifyOnReOpenedIncident { get; set; } - - /// - /// How to notify when an user have written an error description - /// - public NotificationState NotifyOnUserFeedback { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Users/NotificationState.cs b/src/Server/OneTrueError.Api/Core/Users/NotificationState.cs deleted file mode 100644 index f043193e..00000000 --- a/src/Server/OneTrueError.Api/Core/Users/NotificationState.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace OneTrueError.Api.Core.Users -{ - /// - /// Type of notification to use - /// - public enum NotificationState - { - /// - /// Use global setting - /// - UseGlobalSetting, - - /// - /// Do not notify - /// - Disabled, - - /// - /// By cellphone (text message) - /// - Cellphone, - - /// - /// By email - /// - Email - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Core/Users/Queries/GetUserSettingsResult.cs b/src/Server/OneTrueError.Api/Core/Users/Queries/GetUserSettingsResult.cs deleted file mode 100644 index 894e6269..00000000 --- a/src/Server/OneTrueError.Api/Core/Users/Queries/GetUserSettingsResult.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace OneTrueError.Api.Core.Users.Queries -{ - /// - /// Result for - /// - /// - /// - /// All settings are system wide except for . - /// - /// - public class GetUserSettingsResult - { - /// - /// First name (optional) - /// - public string FirstName { get; set; } - - /// - /// Last name (optional) - /// - public string LastName { get; set; } - - /// - /// Cell phone number (otional, but required for text notifications). - /// - public string MobileNumber { get; set; } - - /// - /// Application specific settings - /// - public NotificationSettings Notifications { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Modules/Tagging/Events/TagAttachedToIncident.cs b/src/Server/OneTrueError.Api/Modules/Tagging/Events/TagAttachedToIncident.cs deleted file mode 100644 index 22471bf6..00000000 --- a/src/Server/OneTrueError.Api/Modules/Tagging/Events/TagAttachedToIncident.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using DotNetCqs; - -namespace OneTrueError.Api.Modules.Tagging.Events -{ - /// - /// New tag(s) have been identified for the processed incident. - /// - public class TagAttachedToIncident : ApplicationEvent - { - /// - /// Creates a new instance of . - /// - /// Incident being processed - /// tags - /// tags - /// incidentId - public TagAttachedToIncident(int incidentId, TagDTO[] tags) - { - if (tags == null) throw new ArgumentNullException("tags"); - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - Tags = tags; - IncidentId = incidentId; - } - - /// - /// Incident being processed - /// - public int IncidentId { get; private set; } - - /// - /// Identified tags - /// - public TagDTO[] Tags { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/NamespaceDoc.cs b/src/Server/OneTrueError.Api/NamespaceDoc.cs deleted file mode 100644 index 7e8f6f7e..00000000 --- a/src/Server/OneTrueError.Api/NamespaceDoc.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Api -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// API - /// - /// - /// The API is based on Command/Queries and events. - /// - /// Commands can be seen as the write model. All operations is done with the - /// help of commands. A command is not an atomic unit, but do in most cases represent an use case. - /// - /// - /// Queries are the read model in the application. They are used to fetch information. Queries are indempotent and - /// may not change - /// application state. - /// - /// - /// Events are used to allow different parts of the application to talk. The publisher are not aware of if there - /// are any - /// subscribers or how many there are. The subscriber have no knowledge about who published the event. - /// - ///

Implementations

- /// - /// There is a tool in the "Tool" root folder which are used to generate Typescript classes from these - /// APIs. The .ts files can be - /// invoked using ajax directly from the web. - /// - /// - /// You can also invoke the DTOs directly from your application using a HTTP client. Serialize the DTO as JSON and - /// then include - /// X-Cqs-Object-Type as a HTTP header. It should contain the assembly qualified type name of the DTO. - /// - /// Basic authentication is used. Thus we recommend that you run the site using SSL. - ///
- [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/OneTrueError.Api.csproj b/src/Server/OneTrueError.Api/OneTrueError.Api.csproj deleted file mode 100644 index 00034ac9..00000000 --- a/src/Server/OneTrueError.Api/OneTrueError.Api.csproj +++ /dev/null @@ -1,244 +0,0 @@ - - - - Debug - AnyCPU - Properties - OneTrueError.Api - 512 - Library - {FC331A95-FCA4-4764-8004-0884665DD01F} - OneTrueError.Api - v4.5.2 - - - true - full - DEBUG;TRACE - bin\Debug\OneTrueError.Api.XML - prompt - false - bin\Debug\ - 4 - - - pdbonly - TRACE - bin\Release\OneTrueError.Api.XML - prompt - true - bin\Release\ - 4 - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.39\lib\net45\Griffin.Core.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/OneTrueError.Api.csproj.DotSettings b/src/Server/OneTrueError.Api/OneTrueError.Api.csproj.DotSettings deleted file mode 100644 index 662f9568..00000000 --- a/src/Server/OneTrueError.Api/OneTrueError.Api.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - CSharp50 \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.Api/Properties/AssemblyInfo.cs deleted file mode 100644 index 9010535d..00000000 --- a/src/Server/OneTrueError.Api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("OneTrueError.Api")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.Api")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("fc331a95-fca4-4764-8004-0884665dd01f")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverview.cs b/src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverview.cs deleted file mode 100644 index e0bdf407..00000000 --- a/src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverview.cs +++ /dev/null @@ -1,18 +0,0 @@ -using DotNetCqs; - -namespace OneTrueError.Api.Web.Overview.Queries -{ - /// - /// Get an OneTrueError summary (typically shown in the chart and right panel summary) - /// - public class GetOverview : Query - { - /// - /// Amount of time to look back (i.e. startdate = DateTime.Now.Substract(WindowSize)) - /// - /// - /// 1 = switch to hours - /// - public int NumberOfDays { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverviewResult.cs b/src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverviewResult.cs deleted file mode 100644 index 4895ed89..00000000 --- a/src/Server/OneTrueError.Api/Web/Overview/Queries/GetOverviewResult.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace OneTrueError.Api.Web.Overview.Queries -{ - /// - /// Result for . - /// - public class GetOverviewResult - { - /// - /// 1 = switch to hours for incidents and reports. - /// - public int Days { get; set; } - - /// - /// One collection per application - /// - public GetOverviewApplicationResult[] IncidentsPerApplication { get; set; } - - /// - /// Aggregated summary - /// - public OverviewStatSummary StatSummary { get; set; } - - /// - /// Labels for the time axis (X-axis) in the chart. - /// - public string[] TimeAxisLabels { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/Web/Overview/Queries/OverviewStatSummary.cs b/src/Server/OneTrueError.Api/Web/Overview/Queries/OverviewStatSummary.cs deleted file mode 100644 index 994f7d37..00000000 --- a/src/Server/OneTrueError.Api/Web/Overview/Queries/OverviewStatSummary.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace OneTrueError.Api.Web.Overview.Queries -{ - /// - /// Stats for the last X days, part of . - /// - public class OverviewStatSummary - { - /// - /// Number of followers - /// - public int Followers { get; set; } - - /// - /// Number of incidents - /// - public int Incidents { get; set; } - - /// - /// Number of reports received - /// - public int Reports { get; set; } - - /// - /// Number user feedback items - /// - public int UserFeedback { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Api/packages.config b/src/Server/OneTrueError.Api/packages.config deleted file mode 100644 index c99b8683..00000000 --- a/src/Server/OneTrueError.Api/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Configuration/AppConfigStoreTests.cs b/src/Server/OneTrueError.App.Tests/Configuration/AppConfigStoreTests.cs deleted file mode 100644 index 0f1b25cf..00000000 --- a/src/Server/OneTrueError.App.Tests/Configuration/AppConfigStoreTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Configuration; -using FluentAssertions; -using OneTrueError.App.Tests.Configuration.TestEntitites; -using OneTrueError.Infrastructure.Configuration.ConfigFile; -using Xunit; - -namespace OneTrueError.App.Tests.Configuration -{ - public class AppConfigStoreTests - { - [Fact] - public void should_be_able_to_read_an_existing_category() - { - var sut = new ConfigFileStore(); - var actual = sut.Load(); - - actual.Name.Should().Be("Yo!"); - } - - [Fact] - public void should_return_null_if_a_category_is_missing() - { - var sut = new ConfigFileStore(); - var actual = sut.Load(); - - actual.Should().BeNull(); - } - - [Fact] - public void should_store_settings_with_dot_notation() - { - var expected = "World" + Guid.NewGuid().ToString("N"); - var category = new WriteTestSection(); - category.Properties["Hello"] = expected; - - var sut = new ConfigFileStore(); - sut.Store(category); - var actual = ConfigurationManager.AppSettings[category.SectionName + ".Hello"]; - - actual.Should().Be(expected); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Configuration/DictionaryExtensionsTests.cs b/src/Server/OneTrueError.App.Tests/Configuration/DictionaryExtensionsTests.cs deleted file mode 100644 index a95ff4d2..00000000 --- a/src/Server/OneTrueError.App.Tests/Configuration/DictionaryExtensionsTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Collections.Generic; -using FluentAssertions; -using OneTrueError.Infrastructure.Configuration; -using Xunit; - -namespace OneTrueError.App.Tests.Configuration -{ - public class DictionaryExtensionsTests - { - #region strings - - [Fact] - public void Should_return_value_if_given_key_exists() - { - var dict = new Dictionary {{"Name", "Vlue"}}; - - var actual = dict.GetString("Name"); - - actual.Should().Be("Vlue"); - } - - [Fact] - public void Should_return_default_Value_if_given_key_do_not_exist() - { - var dict = new Dictionary {{"Name", "Vlue"}}; - - var actual = dict.GetString("Supplier", "Santa"); - - actual.Should().Be("Santa"); - } - - #endregion - - #region boolean - - [Fact] - public void should_include_key_name_if_item_do_not_exist() - { - var dict = new Dictionary {{"Usable", "Vlue"}}; - - Action actual = () => dict.GetBoolean("hey!"); - - actual.ShouldThrow().Which.Message.Contains("hey!"); - } - - [Fact] - public void should_convert_to_bolean_if_given_item_is_found() - { - var dict = new Dictionary {{"Usable", "True"}}; - - var actual = dict.GetBoolean("Usable"); - - actual.Should().BeTrue(); - } - - - [Fact] - public void should_include_value_if_item_is_not_convertable_to_boolean() - { - var dict = new Dictionary {{"Usable", "Vlue"}}; - - Action actual = () => dict.GetBoolean("Usable"); - - actual.ShouldThrow().Which.Message.Contains("Vlue"); - } - - #endregion - - #region integer - - [Fact] - public void should_include_key_name_if_int_item_do_not_exist() - { - var dict = new Dictionary {{"Length", "Vlue"}}; - - Action actual = () => dict.GetInteger("hey!"); - - actual.ShouldThrow().Which.Message.Contains("hey!"); - } - - [Fact] - public void should_convert_to_integer_if_given_item_is_found() - { - var dict = new Dictionary {{"Length", "1100"}}; - - var actual = dict.GetInteger("Length"); - - actual.Should().Be(1100); - } - - - [Fact] - public void should_include_value_if_item_is_not_convertable_to_integer() - { - var dict = new Dictionary {{"Usable", "Vlue"}}; - - Action actual = () => dict.GetInteger("Usable"); - - actual.ShouldThrow().Which.Message.Contains("Vlue"); - } - - #endregion - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/NonExistantSection.cs b/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/NonExistantSection.cs deleted file mode 100644 index 588bc20c..00000000 --- a/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/NonExistantSection.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Tests.Configuration.TestEntitites -{ - public class NonExistantSection : IConfigurationSection - { - public string SectionName - { - get { return "NpNp"; } - } - - public IDictionary ToDictionary() - { - return this.ToConfigDictionary(); - } - - public void Load(IDictionary settings) - { - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/SoCultural.cs b/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/SoCultural.cs deleted file mode 100644 index c8e770ca..00000000 --- a/src/Server/OneTrueError.App.Tests/Configuration/TestEntitites/SoCultural.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Generic; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Tests.Configuration.TestEntitites -{ - internal class SoCultural : IConfigurationSection - { - public float Number { get; set; } - - public string SectionName - { - get { return "SoCultural"; } - } - - public IDictionary ToDictionary() - { - return this.ToConfigDictionary(); - } - - public void Load(IDictionary settings) - { - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Core/Accounts/CommandHandlers/RegisterAccountHandlerTests.cs b/src/Server/OneTrueError.App.Tests/Core/Accounts/CommandHandlers/RegisterAccountHandlerTests.cs deleted file mode 100644 index 659b3b51..00000000 --- a/src/Server/OneTrueError.App.Tests/Core/Accounts/CommandHandlers/RegisterAccountHandlerTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using FluentAssertions; -using NSubstitute; -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Core.Accounts; -using OneTrueError.App.Core.Accounts.CommandHandlers; -using Xunit; - -namespace OneTrueError.App.Tests.Core.Accounts.CommandHandlers -{ - public class RegisterAccountHandlerTests - { - [Fact] - public async Task should_create_a_new_account() - { - var repos = Substitute.For(); - var cmdBus = Substitute.For(); - var eventBus = Substitute.For(); - var cmd = new RegisterAccount("rne", "yo", "someEmal"); - repos.When(x => x.CreateAsync(Arg.Any())) - .Do(x => x.Arg().SetId(3)); - - - var sut = new RegisterAccountHandler(repos, cmdBus, eventBus); - await sut.ExecuteAsync(cmd); - await repos.Received().CreateAsync(Arg.Any()); - } - - [Fact] - public async Task should_inform_the_rest_of_the_system_about_the_new_account() - { - var repos = Substitute.For(); - var cmdBus = Substitute.For(); - var eventBus = Substitute.For(); - var cmd = new RegisterAccount("rne", "yo", "someEmal"); - repos.When(x => x.CreateAsync(Arg.Any())) - .Do(x => x.Arg().SetId(3)); - - - var sut = new RegisterAccountHandler(repos, cmdBus, eventBus); - await sut.ExecuteAsync(cmd); - - await eventBus.Received().PublishAsync(Arg.Any()); - eventBus.Method("PublishAsync").Arg().AccountId.Should().Be(3); - } - - [Fact] - public async Task should_send_activation_email() - { - var repos = Substitute.For(); - var cmdBus = Substitute.For(); - var eventBus = Substitute.For(); - var cmd = new RegisterAccount("rne", "yo", "someEmal"); - repos.When(x => x.CreateAsync(Arg.Any())) - .Do(x => x.Arg().SetId(3)); - - - var sut = new RegisterAccountHandler(repos, cmdBus, eventBus); - await sut.ExecuteAsync(cmd); - - await cmdBus.Received().ExecuteAsync(Arg.Any()); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Core/Applications/Commands/InviteUserHandlerTests.cs b/src/Server/OneTrueError.App.Tests/Core/Applications/Commands/InviteUserHandlerTests.cs deleted file mode 100644 index 5c01bb08..00000000 --- a/src/Server/OneTrueError.App.Tests/Core/Applications/Commands/InviteUserHandlerTests.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using FluentAssertions; -using NSubstitute; -using OneTrueError.Api.Core.Applications.Events; -using OneTrueError.Api.Core.Invitations.Commands; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Core.Applications; -using OneTrueError.App.Core.Invitations; -using OneTrueError.App.Core.Invitations.CommandHandlers; -using OneTrueError.App.Core.Users; -using OneTrueError.Infrastructure.Configuration; -using Xunit; - -namespace OneTrueError.App.Tests.Core.Applications.Commands -{ - public class InviteUserHandlerTests - { - private readonly IApplicationRepository _applicationRepository; - private readonly ICommandBus _commandBus; - private readonly IEventBus _eventBus; - private readonly IInvitationRepository _invitationRepository; - private readonly InviteUserHandler _sut; - private readonly IUserRepository _userRepository; - - public InviteUserHandlerTests() - { - _invitationRepository = Substitute.For(); - _userRepository = Substitute.For(); - _applicationRepository = Substitute.For(); - _commandBus = Substitute.For(); - _eventBus = Substitute.For(); - _userRepository.GetUserAsync(1).Returns(new User(1, "First")); - _applicationRepository.GetByIdAsync(1).Returns(new Application(1, "MyApp")); - ConfigurationStore.Instance = new TestStore(); - _sut = new InviteUserHandler(_invitationRepository, _eventBus, _userRepository, _applicationRepository, - _commandBus); - } - - [Fact] - public async Task should_create_an_invite_for_a_new_user() - { - var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 1}; - var members = new[] {new ApplicationTeamMember(1, 3)}; - ApplicationTeamMember actual = null; - _applicationRepository.GetTeamMembersAsync(1).Returns(members); - _applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any())) - .Do(x => actual = x.Arg()); - - await _sut.ExecuteAsync(cmd); - - _applicationRepository.Received().CreateAsync(Arg.Any()); - actual.EmailAddress.Should().Be(cmd.EmailAddress); - actual.ApplicationId.Should().Be(cmd.ApplicationId); - actual.AddedAtUtc.Should().BeCloseTo(DateTime.UtcNow, 1000); - actual.AddedByName.Should().Be("First"); - } - - [Fact] - public async Task should_not_allow_invites_when_the_invited_user_already_have_an_account() - { - var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 2}; - var members = new[] {new ApplicationTeamMember(1, 3)}; - _userRepository.FindByEmailAsync(cmd.EmailAddress).Returns(new User(3, "existing")); - _applicationRepository.GetTeamMembersAsync(1).Returns(members); - - await _sut.ExecuteAsync(cmd); - - await _applicationRepository.DidNotReceive().CreateAsync(Arg.Any()); - } - - [Fact] - public async Task should_not_allow_invites_when_the_invited_user_already_have_an_pending_invite() - { - var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 1}; - var members = new[] {new ApplicationTeamMember(1, cmd.EmailAddress)}; - _applicationRepository.GetTeamMembersAsync(1).Returns(members); - - await _sut.ExecuteAsync(cmd); - - await _applicationRepository.DidNotReceive().CreateAsync(Arg.Any()); - } - - [Fact] - public async Task should_notify_the_system_of_the_invite() - { - var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 1}; - var members = new[] {new ApplicationTeamMember(1, 3)}; - ApplicationTeamMember actual = null; - _applicationRepository.GetTeamMembersAsync(1).Returns(members); - _applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any())) - .Do(x => actual = x.Arg()); - - await _sut.ExecuteAsync(cmd); - - _applicationRepository.Received().CreateAsync(Arg.Any()); - _eventBus.Received().PublishAsync(Arg.Any()); - } - - [Fact] - public async Task should_send_an_invitation_email() - { - var cmd = new InviteUser(1, "jonas@gauffin.com") {UserId = 1}; - var members = new[] {new ApplicationTeamMember(1, 3)}; - ApplicationTeamMember actual = null; - _applicationRepository.GetTeamMembersAsync(1).Returns(members); - _applicationRepository.WhenForAnyArgs(x => x.CreateAsync(Arg.Any())) - .Do(x => actual = x.Arg()); - - await _sut.ExecuteAsync(cmd); - - _commandBus.Received().ExecuteAsync(Arg.Any()); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Core/Invitations/Commands/AcceptInvitationHandlerTests.cs b/src/Server/OneTrueError.App.Tests/Core/Invitations/Commands/AcceptInvitationHandlerTests.cs deleted file mode 100644 index 4c8cbc2e..00000000 --- a/src/Server/OneTrueError.App.Tests/Core/Invitations/Commands/AcceptInvitationHandlerTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using FluentAssertions; -using NSubstitute; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Accounts.Requests; -using OneTrueError.App.Core.Accounts; -using OneTrueError.App.Core.Invitations; -using OneTrueError.App.Core.Invitations.CommandHandlers; -using Xunit; - -namespace OneTrueError.App.Tests.Core.Invitations.Commands -{ - public class AcceptInvitationHandlerTests - { - private IInvitationRepository _repository; - private IAccountRepository _accountRepository; - private IEventBus _eventBus; - private AcceptInvitationHandler _sut; - private Account _invitedAccount; - private const int InvitedAccountId = 999; - - - public AcceptInvitationHandlerTests() - { - _repository = Substitute.For(); - _accountRepository = Substitute.For(); - _eventBus = Substitute.For(); - _sut = new AcceptInvitationHandler(_repository, _accountRepository, _eventBus); - _invitedAccount = new Account("arne", "1234"); - _invitedAccount.SetId(InvitedAccountId); - _invitedAccount.SetVerifiedEmail("jonas@gauffin.com"); - _accountRepository.GetByIdAsync(InvitedAccountId).Returns(_invitedAccount); - } - - [Fact] - public async Task should_delete_invitation_when_its_accepted_to_prevent_creating_multiple_accounts_with_the_same_invitation_key() - { - var invitation = new Invitation("invited@test.com", "inviter"); - var request = new AcceptInvitation(InvitedAccountId, invitation.InvitationKey) {AcceptedEmail = "arne@gauffin.com"}; - invitation.Add(1, "arne"); - _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); - - var actual = await _sut.ExecuteAsync(request); - - actual.Should().NotBeNull(); - } - - [Fact] - public async Task should_notify_system_of_the_accepted_invitation() - { - var invitation = new Invitation("invited@test.com", "inviter"); - var request = new AcceptInvitation(InvitedAccountId, invitation.InvitationKey) { AcceptedEmail = "arne@gauffin.com" }; - invitation.Add(1, "arne"); - _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); - - var actual = await _sut.ExecuteAsync(request); - - _eventBus.Received().PublishAsync(Arg.Any()); - var evt = _eventBus.Method("PublishAsync").Arg(); - evt.AcceptedEmailAddress.Should().Be(request.AcceptedEmail); - evt.AccountId.Should().Be(InvitedAccountId); - evt.ApplicationIds[0].Should().Be(1); - evt.UserName.Should().Be(_invitedAccount.UserName); - } - - [Fact] - public async Task should_create_an_Account_for_invites_to_new_users() - { - var invitation = new Invitation("invited@test.com", "inviter"); - var request = new AcceptInvitation("arne", "pass", invitation.InvitationKey) { AcceptedEmail = "arne@gauffin.com" }; - invitation.Add(1, "arne"); - _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); - _accountRepository - .WhenForAnyArgs(x => x.CreateAsync(null)) - .Do(x => x.Arg().SetId(52)); - - - var actual = await _sut.ExecuteAsync(request); - - _accountRepository.Received().CreateAsync(Arg.Any()); - var evt = _eventBus.Method("PublishAsync").Arg(); - evt.AccountId.Should().Be(52); - } - - [Fact] - public async Task should_publish_AccountRegistered_if_a_new_account_is_created_as_we_bypass_the_regular_account_registration_flow() - { - var invitation = new Invitation("invited@test.com", "inviter"); - var request = new AcceptInvitation("arne", "pass", invitation.InvitationKey) { AcceptedEmail = "arne@gauffin.com" }; - invitation.Add(1, "arne"); - _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); - _accountRepository - .WhenForAnyArgs(x => x.CreateAsync(null)) - .Do(x => x.Arg().SetId(52)); - - - await _sut.ExecuteAsync(request); - - _eventBus.Received().PublishAsync(Arg.Any()); - var evt = _eventBus.Method("PublishAsync").Arg(); - evt.AccountId.Should().Be(52); - } - - [Fact] - public async Task should_publish_AccountActivated_if_a_new_account_is_created_as_we_bypass_the_regular_account_registration_flow() - { - var invitation = new Invitation("invited@test.com", "inviter"); - var request = new AcceptInvitation("arne", "pass", invitation.InvitationKey) { AcceptedEmail = "arne@gauffin.com" }; - invitation.Add(1, "arne"); - _repository.GetByInvitationKeyAsync(request.InvitationKey).Returns(invitation); - _accountRepository - .WhenForAnyArgs(x => x.CreateAsync(null)) - .Do(x => x.Arg().SetId(52)); - - - await _sut.ExecuteAsync(request); - - _eventBus.Received().PublishAsync(Arg.Any()); - var evt = _eventBus.Method("PublishAsync").Arg(); - evt.AccountId.Should().Be(52); - } - - - [Fact] - public async Task should_ignore_invitations_where_the_key_is_not_registered_in_the_db() - { - var request = new AcceptInvitation(InvitedAccountId, "invalid") { AcceptedEmail = "arne@gauffin.com" }; - - var actual = await _sut.ExecuteAsync(request); - - actual.Should().BeNull(); - } - } -} diff --git a/src/Server/OneTrueError.App.Tests/NSubstittueExtensions.cs b/src/Server/OneTrueError.App.Tests/NSubstittueExtensions.cs deleted file mode 100644 index 2119d49d..00000000 --- a/src/Server/OneTrueError.App.Tests/NSubstittueExtensions.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NSubstitute; -using NSubstitute.Core; - -namespace OneTrueError.App.Tests -{ - internal static class NSubstitueExtensions - { - public static MethodListWrapper Method(this object instance, string methodName) - { - var calls = instance.ReceivedCalls().Where(x => x.GetMethodInfo().Name == methodName).ToList(); - return new MethodListWrapper(instance, calls); - } - - public static MethodWrapper Method(this object instance, string methodName, int indexer) - { - var calls = instance.ReceivedCalls().Where(x => x.GetMethodInfo().Name == methodName).ToList(); - if (calls.Count <= indexer) - throw new InvalidOperationException("There are only " + calls.Count + " calls available, you specified index " + indexer); - - return new MethodWrapper(instance, calls[0]); - } - } - - internal class MethodWrapper - { - private readonly ICall _call; - private readonly object _instance; - - public MethodWrapper(object instance, ICall call) - { - _instance = instance; - _call = call; - } - - public TArgument Arg(int index) - { - if ((index < 0) || (index >= _call.GetArguments().Length)) - throw new InvalidOperationException("Method '" + _call.GetMethodInfo().Name + - "' do not have that many arguments."); - - if (_call.GetArguments()[index] is TArgument) - return (TArgument) _call.GetArguments()[index]; - - throw new InvalidOperationException("Argument " + index + " of '" + _call.GetMethodInfo().Name + - "' cannot be converted to '" + typeof(TArgument) + "'."); - } - - public TArgument Arg() - { - var args = _call.GetArguments().Where(x => x is TArgument).ToList(); - if (args.Count != 1) - throw new InvalidOperationException("More than one argument of type '" + _call.GetMethodInfo().Name + - "' is of type " + typeof(TArgument)); - - return (TArgument) args[0]; - } - } - - internal class MethodListWrapper - { - private readonly IList _calls; - private readonly object _instance; - - public MethodListWrapper(object instance, IList calls) - { - _instance = instance; - _calls = calls; - } - - public TArgument Arg(int index) - { - List values = new List(); - foreach (var call in _calls) - { - if ((index < 0) || (index >= call.GetArguments().Length)) - continue; - - if (call.GetArguments()[index] is TArgument) - values.Add(call.GetArguments()[index]); - } - if (values.Count > 1) - throw new InvalidOperationException("There was multiple calls to "+ _calls.First().GetMethodInfo().Name + " with an argument of type "+ typeof(TArgument) + ". Use method call indexer."); - if (values.Count == 1) - return (TArgument) values[0]; - - throw new InvalidOperationException("None of the calls to '" + _calls.First().GetMethodInfo().Name + "' had an argument of type " + typeof(TArgument)); - } - - public TArgument Arg() - { - List values = new List(); - foreach (var call in _calls) - { - var arg = call.GetArguments().FirstOrDefault(x => x.GetType() == typeof(TArgument)); - if (arg != null) - values.Add(arg); - } - if (values.Count > 1) - throw new InvalidOperationException("There was multiple calls to " + _calls.First().GetMethodInfo().Name + " with an argument of type " + typeof(TArgument) + ". Use method call indexer."); - if (values.Count == 1) - return (TArgument) values[0]; - - throw new InvalidOperationException("None of the calls to '" + _calls.First().GetMethodInfo().Name + "' had an argument of type " + typeof(TArgument)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/OneTrueError.App.Tests.csproj b/src/Server/OneTrueError.App.Tests/OneTrueError.App.Tests.csproj deleted file mode 100644 index b0b27077..00000000 --- a/src/Server/OneTrueError.App.Tests/OneTrueError.App.Tests.csproj +++ /dev/null @@ -1,162 +0,0 @@ - - - - Debug - AnyCPU - {9031CF08-2778-487B-8E11-2F45714875D1} - Library - Properties - OneTrueError.App.Tests - OneTrueError.App.Tests - v4.5.2 - 512 - {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages - False - UnitTest - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - ManagedMinimumRules.ruleset - 4014 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\FluentAssertions.4.14.0\lib\net45\FluentAssertions.dll - True - - - ..\packages\FluentAssertions.4.14.0\lib\net45\FluentAssertions.Core.dll - True - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.39\lib\net45\Griffin.Core.dll - True - - - ..\packages\NSubstitute.1.10.0.0\lib\net45\NSubstitute.dll - True - - - - - - - ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll - True - - - ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll - True - - - ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll - True - - - ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {FC331A95-FCA4-4764-8004-0884665DD01F} - OneTrueError.Api - - - {5EF42A74-9323-49FA-A1F6-974D6DE77202} - OneTrueError.App - - - {a78a50da-c9d7-47f2-8528-d7ee39d91924} - OneTrueError.Infrastructure - - - - - - - False - - - False - - - False - - - False - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.App.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 1e6461ed..00000000 --- a/src/Server/OneTrueError.App.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("OneTrueError.App.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.App.Tests")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("9031cf08-2778-487b-8e11-2f45714875d1")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/TestStore.cs b/src/Server/OneTrueError.App.Tests/TestStore.cs deleted file mode 100644 index c4db28b7..00000000 --- a/src/Server/OneTrueError.App.Tests/TestStore.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Tests -{ - public class TestStore : ConfigurationStore - { - public override T Load() - { - if (typeof(T) == typeof(BaseConfiguration)) - return (T) (object) new BaseConfiguration {BaseUrl = new Uri("http://localhost/")}; - - return new T(); - } - - public override void Store(IConfigurationSection section) - { - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App.Tests/Web/Overview/Queries/GetOverviewApplicationResultTests.cs b/src/Server/OneTrueError.App.Tests/Web/Overview/Queries/GetOverviewApplicationResultTests.cs deleted file mode 100644 index 4d8a197d..00000000 --- a/src/Server/OneTrueError.App.Tests/Web/Overview/Queries/GetOverviewApplicationResultTests.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; -using OneTrueError.Api.Web.Overview.Queries; -using Xunit; - -namespace OneTrueError.App.Tests.Web.Overview.Queries -{ - public class GetOverviewApplicationResultTests - { - - [Fact] - public void ignore_future_dates_to_allow_malconfigured_clients() - { - - var sut = new GetOverviewApplicationResult("Label", DateTime.Today.AddDays(-30), 30); - sut.AddValue(DateTime.Today.AddDays(1), 10); - - } - - [Fact] - public void should_allow_dates_within_the_given_interval() - { - - var sut = new GetOverviewApplicationResult("hello", DateTime.Today.AddDays(-30), 31); - sut.AddValue(DateTime.Today, 10); - - sut.Values[sut.Values.Length - 1].Should().Be(10); - } - } -} diff --git a/src/Server/OneTrueError.App.Tests/packages.config b/src/Server/OneTrueError.App.Tests/packages.config deleted file mode 100644 index 26c4fffb..00000000 --- a/src/Server/OneTrueError.App.Tests/packages.config +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Configuration/BaseConfiguration.cs b/src/Server/OneTrueError.App/Configuration/BaseConfiguration.cs deleted file mode 100644 index c9917a37..00000000 --- a/src/Server/OneTrueError.App/Configuration/BaseConfiguration.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Configuration -{ - /// - /// Base configuration for the OneTrueError service. - /// - public sealed class BaseConfiguration : IConfigurationSection - { - /// - /// Base URL for the home page, including protocol (http:// or https://) - /// - public Uri BaseUrl { get; set; } - - /// - /// Address used as "From" in all emails sent by the system. - /// - public string SenderEmail { get; set; } - - /// - /// Address to contact when having trouble with OneTrueError (account issues etc). - /// - public string SupportEmail { get; set; } - - string IConfigurationSection.SectionName - { - get { return "BaseConfig"; } - } - - IDictionary IConfigurationSection.ToDictionary() - { - return this.ToConfigDictionary(); - } - - void IConfigurationSection.Load(IDictionary settings) - { - this.AssignProperties(settings); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Configuration/OneTrueErrorConfigSection.cs b/src/Server/OneTrueError.App/Configuration/OneTrueErrorConfigSection.cs deleted file mode 100644 index c28c0178..00000000 --- a/src/Server/OneTrueError.App/Configuration/OneTrueErrorConfigSection.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System.Collections.Generic; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Configuration -{ - /// - /// We'll want to track all exceptions for all OTE users so that we can correct bugs in OTE. - /// - public sealed class OneTrueErrorConfigSection : IConfigurationSection - { - /// - /// Allow us to track exceptions in OTE. - /// - public bool ActivateTracking { get; set; } - - /// - /// Email address that we may contact if we need any further information (will also receive notifications when the - /// errors are corrected). - /// - public string ContactEmail { get; set; } - - /// - /// A fixed identity which identifies this specific installation. You can generate a GUID and then store it. - /// - /// - /// - /// Used to identify the number of installations that have the same issue. - /// - /// - public string InstallationId { get; set; } - - string IConfigurationSection.SectionName - { - get { return "ErrorTracking"; } - } - - IDictionary IConfigurationSection.ToDictionary() - { - return this.ToConfigDictionary(); - } - - void IConfigurationSection.Load(IDictionary settings) - { - this.AssignProperties(settings); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Account.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/Account.cs.orig deleted file mode 100644 index 86342cba..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Account.cs.orig +++ /dev/null @@ -1,284 +0,0 @@ -using System; -using System.Security.Authentication; -using System.Security.Cryptography; - -<<<<<<< HEAD -namespace OneTrueError.AccountManagement.App.Accounts -{ -======= -namespace OneTrueError.App.Core.Accounts -{ - /// - /// An account (i.e. just allows a user to login, but do not give access to teams etc). - /// ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - public class Account - { - public const string SEQUENCE = "Accounts"; - public const int MaxPasswordAttempts = 3; - -<<<<<<< HEAD -======= - /// - /// Create a new instance of - - /// - /// User name - /// password ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - public Account(string userName, string password) - { - if (userName == null) throw new ArgumentNullException("userName"); - if (password == null) throw new ArgumentNullException("password"); - - UserName = userName; - CreatedAtUtc = DateTime.UtcNow; - ActivationKey = Guid.NewGuid().ToString("N"); - AccountState = AccountStatus.VerificationRequired; - HashedPassword = HashPassword(password); - } - - protected Account() - { - } - - -<<<<<<< HEAD - public int Id { get; private set; } - public string UserName { get; private set; } - public string HashedPassword { get; private set; } - public string Salt { get; private set; } - public DateTime CreatedAtUtc { get; private set; } - public AccountStatus AccountState { get; private set; } - - /// - /// Campaign that the user participated in - /// - public string PromotionCode { get; set; } -======= - /// - /// Primary key - /// - public int Id { get; private set; } - - /// - /// Username - /// - public string UserName { get; private set; } - - /// - /// Password salted and hashed. - /// - public string HashedPassword { get; private set; } - - /// - /// Password salt. - /// - public string Salt { get; private set; } - - /// - /// When this account was created. - /// - public DateTime CreatedAtUtc { get; private set; } - - /// - /// Current state - /// - public AccountStatus AccountState { get; private set; } ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - /// - /// Private setter since new emails needs to be verifier (verification email with a link) - /// - public string Email { get; set; } - -<<<<<<< HEAD - public int CustomerId { get; set; } - public DateTime UpdatedAtUtc { get; private set; } - - /// - /// Used to verify the mail address - /// - public string ActivationKey { get; private set; } - - public int LoginAttempts { get; private set; } - public DateTime LastLoginAtUtc { get; private set; } - public string TrackingId { get; set; } - -======= - - /// - /// Last time a property was updated. - /// - public DateTime UpdatedAtUtc { get; private set; } - - /// - /// Used to verify the mail address (if verifiaction is activated) - /// - public string ActivationKey { get; private set; } - - /// - /// Number of failed login attempts (reseted on each successfull login attempt). - /// - public int LoginAttempts { get; private set; } - - /// - /// When last successful login attempt was made. - /// - public DateTime LastLoginAtUtc { get; private set; } - - - /// - /// Hash passwprd using the current salt. - /// - /// Password as entered by the user - /// Salted and hashed password ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - private string HashPassword(string password) - { - if (password == null) throw new ArgumentNullException("password"); - var algorithm2 = new Rfc2898DeriveBytes(password, 64); - var salt = algorithm2.Salt; - Salt = Convert.ToBase64String(salt); - var pw = algorithm2.GetBytes(128); - return Convert.ToBase64String(pw); - } - -<<<<<<< HEAD -======= - /// - /// Login - /// - /// Password as specified by the user - /// true if password was the correct one; otherwise false. - /// Account is not active, or too many failed login attempts. ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - public bool Login(string password) - { - if (AccountState == AccountStatus.VerificationRequired) - throw new AuthenticationException("You have to activate your account first. Check your email."); - - if (AccountState == AccountStatus.Locked) - throw new AuthenticationException("Your account has been locked. Contact support."); - - if (LoginAttempts >= MaxPasswordAttempts) - { - throw new AuthenticationException("Too many login attempts."); - } - - // null for cookie logins. - if (password == null) - { - LastLoginAtUtc = DateTime.UtcNow; - LoginAttempts = 0; - return true; -<<<<<<< HEAD - -======= ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - } - - var salt = Convert.FromBase64String(Salt); - var algorithm2 = new Rfc2898DeriveBytes(password, salt); - var pw = algorithm2.GetBytes(128); - - var hashedPw = Convert.ToBase64String(pw); - if (hashedPw == HashedPassword) - { - LastLoginAtUtc = DateTime.UtcNow; - LoginAttempts = 0; - return true; - } - LoginAttempts++; - return false; - } - -<<<<<<< HEAD - public bool ValidatePassword(string password) - { - if (password == null) throw new ArgumentNullException("password"); - var salt = Convert.FromBase64String(Salt); - var algorithm2 = new Rfc2898DeriveBytes(password, salt); - var pw = algorithm2.GetBytes(128); - - var hashedPw = Convert.ToBase64String(pw); - return hashedPw == HashedPassword; - } - -======= - - /// - /// Email has been verified. - /// - /// Email address ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - public void SetVerifiedEmail(string email) - { - if (email == null) throw new ArgumentNullException("email"); - Email = email; - } - -<<<<<<< HEAD -======= - /// - /// Want to reset password. - /// - /// - /// - /// Changes user state to and generates a new - /// . - /// - /// ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - public void RequestPasswordReset() - { - AccountState = AccountStatus.ResetPassword; - ActivationKey = Guid.NewGuid().ToString("N"); - } - -<<<<<<< HEAD -======= - /// - /// Activate account (i.e. allow logins). - /// ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - public void Activate() - { - AccountState = AccountStatus.Active; - ActivationKey = null; - UpdatedAtUtc = DateTime.UtcNow; - LoginAttempts = 0; - LastLoginAtUtc = DateTime.UtcNow; - } - -<<<<<<< HEAD -======= - /// - /// Change password - /// - /// New password as entered by the user. ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - public void ChangePassword(string newPassword) - { - if (newPassword == null) throw new ArgumentNullException("newPassword"); - HashedPassword = HashPassword(newPassword); - ActivationKey = null; - UpdatedAtUtc = DateTime.UtcNow; - AccountState = AccountStatus.Active; - LoginAttempts = 0; - } -<<<<<<< HEAD -======= - - /// - /// Check if the given password is the current one. - /// - /// Password as entered by the user. - /// true if the password is the same as the current one; otherwise false. - public bool ValidatePassword(string enteredPassword) - { - if (enteredPassword == null) throw new ArgumentNullException(nameof(enteredPassword)); - return HashPassword(enteredPassword) == HashedPassword; - } ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/AccountStatus.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/AccountStatus.cs.orig deleted file mode 100644 index 607eb0ce..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/AccountStatus.cs.orig +++ /dev/null @@ -1,14 +0,0 @@ -<<<<<<< HEAD -namespace OneTrueError.AccountManagement.App.Accounts -======= -namespace OneTrueError.App.Core.Accounts ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 -{ - public enum AccountStatus - { - VerificationRequired, - Active, - Locked, - ResetPassword - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs b/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs deleted file mode 100644 index e704ebfc..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.App.Core.Accounts.CommandHandlers.Entities -{ - /// - /// "private" entities which should not be exposed to the rest of the system. - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs.orig deleted file mode 100644 index d1b7e290..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/Entities/NamespaceDoc.cs.orig +++ /dev/null @@ -1,16 +0,0 @@ -using System.Runtime.CompilerServices; - -<<<<<<< HEAD -namespace OneTrueError.AccountManagement.App.Accounts.CommandHandlers.Entities -======= -namespace OneTrueError.App.Core.Accounts.CommandHandlers.Entities ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 -{ - /// - /// "private" entities which should not be exposed to the rest of the system. - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs b/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs deleted file mode 100644 index 94f21baa..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Accounts.CommandHandlers -{ - /// - /// Register a new account. - /// - [Component] - public class RegisterAccountHandler : ICommandHandler - { - private readonly ICommandBus _commandBus; - private readonly IEventBus _eventBus; - private readonly IAccountRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// to send verification email - /// to send . - public RegisterAccountHandler(IAccountRepository repository, ICommandBus commandBus, IEventBus eventBus) - { - _repository = repository; - _commandBus = commandBus; - _eventBus = eventBus; - } - - /// - /// Execute a command asynchronously. - /// - /// Command to execute. - /// - /// Task which will be completed once the command has been executed. - /// - public async Task ExecuteAsync(RegisterAccount command) - { - if (command == null) throw new ArgumentNullException("command"); - - if (await _repository.IsUserNameTakenAsync(command.UserName)) - { - await SendAccountInfo(command.UserName); - return; - } - - var account = new Account(command.UserName, command.Password); - account.SetVerifiedEmail(command.Email); - await _repository.CreateAsync(account); - await SendVerificationEmail(account); - var evt = new AccountRegistered(account.Id, account.UserName); - await _eventBus.PublishAsync(evt); - } - -#pragma warning disable 1998 - private async Task SendAccountInfo(string userName) -#pragma warning restore 1998 - { - //TODO: Send information that states - //that an account already exist, with instructions on how to reset the account. - } - - private Task SendVerificationEmail(Account account) - { - var config = ConfigurationStore.Instance.Load(); - //TODO: HTML email - var msg = new EmailMessage - { - TextBody = string.Format(@"Welcome, - - -Your activation code is: {0} - -You can activate your account by clicking on: {1}/account/activate/{0} - -Good luck, - OneTrueError Team", account.ActivationKey, config.BaseUrl), - Subject = "OneTrueError activation" - }; - msg.Recipients = new[] {new EmailAddress(account.Email)}; - - return _commandBus.ExecuteAsync(new SendEmail(msg)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs.orig deleted file mode 100644 index 9831badd..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterAccountHandler.cs.orig +++ /dev/null @@ -1,91 +0,0 @@ -using System.Configuration; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -<<<<<<< HEAD -using OneTrueError.AccountManagement.Api.Messaging; -using OneTrueError.AccountManagement.Api.Messaging.Commands; -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Accounts.Events; - -namespace OneTrueError.AccountManagement.App.Accounts.CommandHandlers -{ - /// - /// Regga nytt konto, skicka ut -======= -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; - -namespace OneTrueError.App.Core.Accounts.CommandHandlers -{ - /// - /// Register a new account. ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - /// - [Component] - public class RegisterAccountHandler : ICommandHandler - { - private readonly ICommandBus _commandBus; - private readonly IEventBus _eventBus; - private readonly IAccountRepository _repository; -<<<<<<< HEAD - //private readonly IIdGeneratorClient _idGeneratorClient; -======= ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - public RegisterAccountHandler(IAccountRepository repository, ICommandBus commandBus, IEventBus eventBus) - { - _repository = repository; -<<<<<<< HEAD - //_idGeneratorClient = idGeneratorClient; -======= ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - _commandBus = commandBus; - _eventBus = eventBus; - } - - public async Task ExecuteAsync(RegisterAccount command) - { -<<<<<<< HEAD - //var id = _idGeneratorClient.GetNextId(Account.SEQUENCE); - var account = new Account(command.UserName, command.Password); - account.SetVerifiedEmail(command.Email); - account.PromotionCode = command.PromotionCode; - account.TrackingId = command.TrackingCookie; - await _repository.CreateAsync(account); - await SendVerificationEmail(account); - var evt = new AccountRegistered(account.Id, account.UserName, account.PromotionCode); -======= - var account = new Account(command.UserName, command.Password); - account.SetVerifiedEmail(command.Email); - await _repository.CreateAsync(account); - await SendVerificationEmail(account); - var evt = new AccountRegistered(account.Id, account.UserName); ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - await _eventBus.PublishAsync(evt); - } - - private Task SendVerificationEmail(Account account) - { - //TODO: HTML email - var msg = new EmailMessage - { - TextBody = string.Format(@"Welcome, - - -Your activation code is: {0} - -You can activate your account by clicking on: {1}/account/activate/{0} - -Good luck, - OneTrueError Team", account.ActivationKey, ConfigurationManager.AppSettings["AppUrl"]), - Subject = "OneTrueError activation" - }; - msg.Recipients = new []{new EmailAddress(account.Email)}; - - return _commandBus.ExecuteAsync(new SendEmail(msg)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs b/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs deleted file mode 100644 index 0f747af9..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Accounts; -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Accounts.CommandHandlers -{ - /// - /// Handler for . - /// - [Component] - internal class RegisterSimpleHandler : ICommandHandler - { - private readonly ICommandBus _commandBus; - private readonly IEventBus _eventBus; - private readonly ILog _logger = LogManager.GetLogger(typeof(RegisterSimpleHandler)); - private readonly IAccountRepository _repository; - - public RegisterSimpleHandler(IAccountRepository repository, ICommandBus commandBus, IEventBus eventBus) - { - _repository = repository; - _commandBus = commandBus; - _eventBus = eventBus; - } - - public async Task ExecuteAsync(RegisterSimple command) - { - var pos = command.EmailAddress.IndexOf('@'); - if (pos == -1) - { - _logger.Warn("Invalid email address: " + command.EmailAddress); - throw new InvalidOperationException("Invalid email address"); - } - - var user = _repository.FindByEmailAsync(command.EmailAddress); - if (user != null) - { - _logger.Warn("Email already taken, sending reset password: " + command.EmailAddress); - await _commandBus.ExecuteAsync(new RequestPasswordReset(command.EmailAddress)); - } - - var userName = await TryCreateUsernameAsync(command, pos); - if (userName == null) - { - _logger.Error("Failed to generate username for " + command.EmailAddress); - return; - } - - - //var id = _idGeneratorClient.GetNextId(Account.SEQUENCE); - var password = Guid.NewGuid().ToString("N").Substring(0, 10); - var account = new Account(userName, password); - account.SetVerifiedEmail(command.EmailAddress); - await _repository.CreateAsync(account); - - await SendAccountEmail(account, password); - - var evt = new AccountRegistered(account.Id, account.UserName); - await _eventBus.PublishAsync(evt); - } - - private Task SendAccountEmail(Account account, string password) - { - var config = ConfigurationStore.Instance.Load(); - //TODO: HTML email - var msg = new EmailMessage - { - TextBody = string.Format(@"Welcome, - - -We have created your account. - -UserName: {1} -Password: {2} - -You can login using {0}/account/activate/{3}. - -We recommend that you change your password before doing something useful. - -Thanks, - The OneTrueError Team", config.BaseUrl, account.UserName, password, account.ActivationKey), - Subject = "OneTrueError activation" - }; - msg.Recipients = new[] {new EmailAddress(account.Email)}; - - return _commandBus.ExecuteAsync(new SendEmail(msg)); - } - - private async Task TryCreateUsernameAsync(RegisterSimple command, int pos) - { - var suggestedUserName = command.EmailAddress.Substring(0, pos); - if (!await _repository.IsUserNameTakenAsync(suggestedUserName)) - return suggestedUserName; - - var counter = 100; - var newUserName = suggestedUserName + counter; - while (counter < 110) - { - if (!await _repository.IsUserNameTakenAsync(newUserName)) - { - suggestedUserName = newUserName; - return suggestedUserName; - } - - counter++; - newUserName = suggestedUserName + counter; - } - - _logger.Error("Failed to generate userName: " + suggestedUserName); - return null; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs.orig deleted file mode 100644 index 76f3f94e..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RegisterSimpleHandler.cs.orig +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Configuration; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -<<<<<<< HEAD -using OneTrueError.AccountManagement.Api.Messaging; -using OneTrueError.AccountManagement.Api.Messaging.Commands; -using OneTrueError.Api.Core.Accounts; -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Accounts.Events; - -namespace OneTrueError.AccountManagement.App.Accounts.CommandHandlers -{ -======= -using OneTrueError.Api.Core.Accounts; -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; - -namespace OneTrueError.App.Core.Accounts.CommandHandlers -{ - /// - /// Handler for . - /// ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - [Component] - internal class RegisterSimpleHandler : ICommandHandler - { - private readonly ICommandBus _commandBus; - private readonly IEventBus _eventBus; - private readonly ILog _logger = LogManager.GetLogger(typeof (RegisterSimpleHandler)); - private readonly IAccountRepository _repository; - - public RegisterSimpleHandler(IAccountRepository repository, ICommandBus commandBus, IEventBus eventBus) - { - _repository = repository; - _commandBus = commandBus; - _eventBus = eventBus; - } - - public async Task ExecuteAsync(RegisterSimple command) - { - var pos = command.EmailAddress.IndexOf('@'); - if (pos == -1) - { - _logger.Warn("Invalid email address: " + command.EmailAddress); - throw new InvalidOperationException("Invalid email address"); - } - - var user = _repository.FindByEmail(command.EmailAddress); - if (user != null) - { - _logger.Warn("Email already taken, sending reset password: " + command.EmailAddress); - await _commandBus.ExecuteAsync(new RequestPasswordReset(command.EmailAddress)); - } - - string userName; - if (!TryCreateUsername(command, pos, out userName)) - { -<<<<<<< HEAD - _logger.Error("Failed to generate username for " + command.EmailAddress); -======= - _logger.Error("Failed to generate username for " + command.EmailAddress); ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - return; - } - - - //var id = _idGeneratorClient.GetNextId(Account.SEQUENCE); - var password = Guid.NewGuid().ToString("N").Substring(0, 10); - var account = new Account(userName, password); - account.SetVerifiedEmail(command.EmailAddress); -<<<<<<< HEAD - account.TrackingId = command.TrackingId; -======= ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - await _repository.CreateAsync(account); - - await SendAccountEmail(account, password); - -<<<<<<< HEAD - var evt = new AccountRegistered(account.Id, account.UserName, account.PromotionCode); -======= - var evt = new AccountRegistered(account.Id, account.UserName); ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - await _eventBus.PublishAsync(evt); - } - - private bool TryCreateUsername(RegisterSimple command, int pos, out string userName) - { - userName = command.EmailAddress.Substring(0, pos); - if (!_repository.IsUserNameTaken(userName)) - return true; - - var counter = 100; - var newUserName = userName + counter; - while (counter < 110) - { - if (!_repository.IsUserNameTaken(newUserName)) - { - userName = newUserName; - return true; - } - - counter++; - newUserName = userName + counter; - } - - _logger.Error("Failed to generate userName: " + userName); - return false; - } - - private Task SendAccountEmail(Account account, string password) - { - //TODO: HTML email - var msg = new EmailMessage - { - TextBody = string.Format(@"Welcome, - - -We have created your account. - -UserName: {1} -Password: {2} - -You can login using {0}/account/activate/{3}. - -<<<<<<< HEAD -We recommend that you change your password before activating a paid subscription. -======= -We recommend that you change your password before doing something useful. ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - -Thanks, - The OneTrueError Team", ConfigurationManager.AppSettings["AppUrl"], account.UserName, password, account.ActivationKey), - Subject = "OneTrueError activation" - }; -<<<<<<< HEAD - msg.Recipients = new []{new EmailAddress(account.Email)}; -======= - msg.Recipients = new[] {new EmailAddress(account.Email)}; ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - return _commandBus.ExecuteAsync(new SendEmail(msg)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs b/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs deleted file mode 100644 index fac9b45a..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Accounts.CommandHandlers -{ - /// - /// Handler for . - /// - [Component] - internal class RequestPasswordResetHandler : ICommandHandler - { - private readonly IAccountRepository _accountRepository; - private readonly ICommandBus _commandBus; - private readonly ILog _logger = LogManager.GetLogger(typeof(RequestPasswordResetHandler)); - - public RequestPasswordResetHandler(IAccountRepository accountRepository, ICommandBus commandBus) - { - _accountRepository = accountRepository; - _commandBus = commandBus; - } - - public async Task ExecuteAsync(RequestPasswordReset command) - { - var account = await _accountRepository.FindByEmailAsync(command.EmailAddress); - if (account == null) - { - _logger.Warn("Failed to find a user with email " + command.EmailAddress); - return; - } - - account.RequestPasswordReset(); - await _accountRepository.UpdateAsync(account); - - var config = ConfigurationStore.Instance.Load(); - var cmd = new SendTemplateEmail("Password reset", "ResetPassword") - { - To = account.Email, - Model = - new - { - AccountName = account.UserName, - ResetLink = //TODO: Remove app settings dependency - config.BaseUrl + "/password/reset/" + - account.ActivationKey - }, - Subject = "Reset password" - }; - - await _commandBus.ExecuteAsync(cmd); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs.orig deleted file mode 100644 index 990b0900..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/CommandHandlers/RequestPasswordResetHandler.cs.orig +++ /dev/null @@ -1,78 +0,0 @@ -using System.Configuration; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -<<<<<<< HEAD -using OneTrueError.AccountManagement.Api.Messaging.Commands; -using OneTrueError.Api.Core.Accounts.Commands; - -namespace OneTrueError.AccountManagement.App.Accounts.CommandHandlers -{ - [Component] - class RequestPasswordResetHandler : ICommandHandler - { - private IAccountRepository _accountRepository; - private readonly ICommandBus _commandBus; - private ILog _logger = LogManager.GetLogger(typeof(RequestPasswordResetHandler)); -======= -using OneTrueError.Api.Core.Accounts.Commands; -using OneTrueError.Api.Core.Messaging.Commands; - -namespace OneTrueError.App.Core.Accounts.CommandHandlers -{ - /// - /// Handler for . - /// - [Component] - internal class RequestPasswordResetHandler : ICommandHandler - { - private readonly ICommandBus _commandBus; - private readonly IAccountRepository _accountRepository; - private readonly ILog _logger = LogManager.GetLogger(typeof (RequestPasswordResetHandler)); ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - public RequestPasswordResetHandler(IAccountRepository accountRepository, ICommandBus commandBus) - { - _accountRepository = accountRepository; - _commandBus = commandBus; - } - - public async Task ExecuteAsync(RequestPasswordReset command) - { - var account = _accountRepository.FindByEmail(command.EmailAddress); - if (account == null) - { - _logger.Warn("Failed to find a user with email " + command.EmailAddress); - return; - } - - account.RequestPasswordReset(); - await _accountRepository.UpdateAsync(account); - - var cmd = new SendTemplateEmail("Password reset", "ResetPassword") - { - To = account.Email, - Model = - new - { - AccountName = account.UserName, - ResetLink = - ConfigurationManager.AppSettings["AppUrl"] + "/password/reset/" + - account.ActivationKey - }, -<<<<<<< HEAD - Subject = "Reset password", -======= - Subject = "Reset password" ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - }; - - await _commandBus.ExecuteAsync(cmd); - } - } -<<<<<<< HEAD -} -======= -} ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 diff --git a/src/Server/OneTrueError.App/Core/Accounts/GetUserQueryHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/GetUserQueryHandler.cs.orig deleted file mode 100644 index 77156091..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/GetUserQueryHandler.cs.orig +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -<<<<<<< HEAD -using OneTrueError.Api.Core.Accounts.Queries; - -namespace OneTrueError.AccountManagement.App.Accounts -{ -======= -using OneTrueError.Api.Core; -using OneTrueError.Api.Core.Accounts.Queries; - -namespace OneTrueError.App.Core.Accounts -{ - /// - /// Handler for . - /// ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - [Component] - public class GetAccountQueryHandler : IQueryHandler - { - private IAccountRepository _repository; - - public GetAccountQueryHandler(IAccountRepository repository) - { - _repository = repository; - } - - public async Task ExecuteAsync(GetAccountById query) - { - var account = await _repository.GetByIdAsync(query.AccountId); - if (account == null) - return null; - - var dto = new AccountDTO() - { - CreatedAtUtc = account.CreatedAtUtc, - Email = account.Email, - Id = account.Id, - LastLoginAtUtc = account.LastLoginAtUtc, -<<<<<<< HEAD - PromotionCode = account.PromotionCode, - State = (AccountState)Enum.Parse(typeof(AccountState), account.AccountState.ToString()), -======= - State = account.AccountState.ConvertEnum(), ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - UpdatedAtUtc = account.UpdatedAtUtc, - UserName = account.UserName - }; - - return dto; - } - } -} diff --git a/src/Server/OneTrueError.App/Core/Accounts/IAccountRepository.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/IAccountRepository.cs.orig deleted file mode 100644 index 41903454..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/IAccountRepository.cs.orig +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; - -<<<<<<< HEAD -namespace OneTrueError.AccountManagement.App.Accounts -======= -namespace OneTrueError.App.Core.Accounts ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 -{ - public interface IAccountRepository - { - Task CreateAsync(Account account); - Account FindByEmail(string emailAddress); - Task FindByUsernameAsync(string userName); - Account FindByActivationKey(string activationKey); - Task GetByIdAsync(int id); - IEnumerable GetById(int[] ids); -<<<<<<< HEAD - bool IsEmailAddressTaken(string email); - bool IsUserNameTaken(string userName); - Task LoggedInAsync(string userName); -======= - - /// - /// Check if email address is taken - /// - /// email - /// true if it exists; otherwise false. - bool IsEmailAddressTaken(string email); - - - /// - /// Check if username is already taken. - /// - /// Username - /// true if it exists; otherwise false. - bool IsUserNameTaken(string userName); - - /// - /// Mark user as logged in. - /// - /// Username - /// task - Task LoggedInAsync(string userName); - ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - Task UpdateAsync(Account account); - Account GetByUserName(string userName); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/NamespaceDoc.cs b/src/Server/OneTrueError.App/Core/Accounts/NamespaceDoc.cs deleted file mode 100644 index 2b8f6aa7..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/NamespaceDoc.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.App.Core.Accounts -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Accounts - /// - /// - /// Accounts are used to handle authentication and site wide authorization. - /// - /// - [CompilerGenerated] - class NamespaceDoc - { - } -} diff --git a/src/Server/OneTrueError.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs.orig deleted file mode 100644 index 0a9e793b..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Queries/FindAccountByUserNameHandler.cs.orig +++ /dev/null @@ -1,28 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Queries; - -<<<<<<< HEAD -namespace OneTrueError.AccountManagement.App.Accounts.Queries -======= -namespace OneTrueError.App.Core.Accounts.Queries ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 -{ - [Component] - public class FindAccountByUserNameHandler : IQueryHandler - { - private readonly IAccountRepository _repository; - - public FindAccountByUserNameHandler(IAccountRepository repository) - { - _repository = repository; - } - - public async Task ExecuteAsync(FindAccountByUserName query) - { - var user = await _repository.FindByUsernameAsync(query.UserName); - return user == null ? null : new UserInfo(user.Id, user.UserName); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Queries/GetAccountQueryHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/Queries/GetAccountQueryHandler.cs.orig deleted file mode 100644 index d864df73..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Queries/GetAccountQueryHandler.cs.orig +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Queries; - -<<<<<<< HEAD -namespace OneTrueError.AccountManagement.App.Accounts.Queries -======= -namespace OneTrueError.App.Core.Accounts.Queries ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 -{ - [Component] - public class GetAccountQueryHandler : IQueryHandler - { - private IAccountRepository _repository; - - public GetAccountQueryHandler(IAccountRepository repository) - { - _repository = repository; - } - - public async Task ExecuteAsync(GetAccountById query) - { - var account = await _repository.GetByIdAsync(query.AccountId); - if (account == null) - return null; - - var dto = new AccountDTO() - { - CreatedAtUtc = account.CreatedAtUtc, - Email = account.Email, - Id = account.Id, - LastLoginAtUtc = account.LastLoginAtUtc, -<<<<<<< HEAD - PromotionCode = account.PromotionCode, -======= ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - State = (AccountState)Enum.Parse(typeof(AccountState), account.AccountState.ToString()), - UpdatedAtUtc = account.UpdatedAtUtc, - UserName = account.UserName - }; - - return dto; - } - } -} diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/ActivateAccountHandler.cs b/src/Server/OneTrueError.App/Core/Accounts/Requests/ActivateAccountHandler.cs deleted file mode 100644 index 52369e61..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/ActivateAccountHandler.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Linq; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Accounts.Requests; -using OneTrueError.Api.Core.Applications.Queries; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// - [Component] - public class ActivateAccountHandler : IRequestHandler - { - private readonly IEventBus _eventBus; - private readonly IQueryBus _queryBus; - private readonly IAccountRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// used to publish . - /// - public ActivateAccountHandler(IAccountRepository repository, IEventBus eventBus, IQueryBus queryBus) - { - _repository = repository; - _eventBus = eventBus; - _queryBus = queryBus; - } - - /// - /// Execute the request and generate a reply. - /// - /// Request to execute - /// - /// Task which will contain the reply once completed. - /// - public async Task ExecuteAsync(ActivateAccount request) - { - var account = await _repository.FindByActivationKeyAsync(request.ActivationKey); - if (account == null) - throw new ArgumentOutOfRangeException("ActivationKey", request.ActivationKey, - "Key was not found."); - - account.Activate(); - await _repository.UpdateAsync(account); - - var query = new GetApplicationList {AccountId = account.Id}; - var apps = await _queryBus.QueryAsync(query); - var claims = - apps.Select(x => new Claim(OneTrueClaims.Application, x.Id.ToString(), ClaimValueTypes.Integer32)) - .ToArray(); - - if (ClaimsPrincipal.Current.IsAccount(account.Id)) - { - var context = new PrincipalFactoryContext(account.Id, account.UserName, new string[0]) { Claims = claims }; - var identity = await PrincipalFactory.CreateAsync(context); - identity.AddUpdateCredentialClaim(); - Thread.CurrentPrincipal = identity; - } - - var evt = new AccountActivated(account.Id, account.UserName) - { - EmailAddress = account.Email - }; - await _eventBus.PublishAsync(evt); - - return new ActivateAccountReply(account.Id, account.UserName); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/ActivateAccountHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/Requests/ActivateAccountHandler.cs.orig deleted file mode 100644 index 3e3e91e7..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/ActivateAccountHandler.cs.orig +++ /dev/null @@ -1,118 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -<<<<<<< HEAD -using OneTrueError.AccountManagement.App.Customers; -using OneTrueError.Api.Core.Accounts; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Accounts.Requests; -using OneTrueError.App; - -namespace OneTrueError.AccountManagement.App.Accounts.Requests -{ - /// - /// Aktivera konto - /// - /// - /// Will also create an organization if the specified organization name is available - /// - [Component] - public class ActivateAccountHandler : IRequestHandler - { - private readonly IAccountRepository _repository; - private readonly ICustomerRepository _customerRepository; - private readonly IEventBus _eventBus; - private ILog _logger = LogManager.GetLogger(typeof(ActivateAccountHandler)); - - public ActivateAccountHandler(IAccountRepository repository, - ICustomerRepository customerRepository, - IEventBus eventBus) - { - _repository = repository; - _customerRepository = customerRepository; -======= -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Accounts.Requests; - -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// - [Component] - public class ActivateAccountHandler : IRequestHandler - { - private readonly IEventBus _eventBus; - private readonly IAccountRepository _repository; - private ILog _logger = LogManager.GetLogger(typeof (ActivateAccountHandler)); - - public ActivateAccountHandler(IAccountRepository repository, IEventBus eventBus) - { - _repository = repository; ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - _eventBus = eventBus; - } - - public async Task ExecuteAsync(ActivateAccount command) - { -<<<<<<< HEAD - Account account = _repository.FindByActivationKey(command.ActivationKey); - if (account == null) - throw new ArgumentOutOfRangeException("command.ActivationKey", command.ActivationKey, "Key was not found."); - - var pos = account.Email.IndexOf('@'); - var dot = account.Email.IndexOf('.', pos); - var customerName = account.Email.Substring(pos + 1, dot - pos - 1); - if (_customerRepository.Exists(customerName)) - { - _logger.Warn("Exists: " + customerName); - customerName = account.UserName; - } - - var customer = new Customer(new AccountLink(account.Id, account.UserName), customerName); - await _customerRepository.CreateAsync(customer); - account.CustomerId = customer.Id; - account.Activate(); - Thread.CurrentPrincipal = new OneTruePrincipal(customer.Id, account.UserName); - await _repository.UpdateAsync(account); - - var evt = new AccountActivated(account.Id, account.UserName, account.PromotionCode, customer.Id) -======= - var account = _repository.FindByActivationKey(command.ActivationKey); - if (account == null) - throw new ArgumentOutOfRangeException("ActivationKey", command.ActivationKey, - "Key was not found."); - - account.Activate(); - await _repository.UpdateAsync(account); - - Thread.CurrentPrincipal = new OneTruePrincipal(0, account.UserName); - var evt = new AccountActivated(account.Id, account.UserName) ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - { - EmailAddress = account.Email - }; - await _eventBus.PublishAsync(evt); - - return new ActivateAccountReply - { - AccountId = account.Id, -<<<<<<< HEAD - CustomerName = customer.Name, - CustomerId = customer.Id, -======= ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - IsOnTrial = true, - TrialDaysLeft = 14, - UserName = account.UserName - }; - } -<<<<<<< HEAD - -======= ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/ChangePasswordHandler.cs b/src/Server/OneTrueError.App/Core/Accounts/Requests/ChangePasswordHandler.cs deleted file mode 100644 index fd174657..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/ChangePasswordHandler.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Requests; - -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// - [Component] - public class ChangePasswordHandler : IRequestHandler - { - private readonly IAccountRepository _repository; - - /// - /// Create a new instance of . - /// - /// Used to load/update the account. - public ChangePasswordHandler(IAccountRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - - _repository = repository; - } - - /// - /// Execute the request and generate a reply. - /// - /// Request to execute - /// - /// Task which will contain the reply once completed. - /// - public async Task ExecuteAsync(ChangePassword request) - { - if (request == null) throw new ArgumentNullException("request"); - - var user = await _repository.GetByIdAsync(request.UserId); - if (!user.ValidatePassword(request.CurrentPassword)) - return new ChangePasswordReply {Success = false}; - - user.ChangePassword(request.NewPassword); - await _repository.UpdateAsync(user); - return new ChangePasswordReply {Success = true}; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/ChangePasswordHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/Requests/ChangePasswordHandler.cs.orig deleted file mode 100644 index 4b8cf55d..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/ChangePasswordHandler.cs.orig +++ /dev/null @@ -1,44 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Requests; - -<<<<<<< HEAD -namespace OneTrueError.AccountManagement.App.Accounts.Requests -{ - [Component] - class ChangePasswordHandler : IRequestHandler -======= -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// - [Component] - public class ChangePasswordHandler : IRequestHandler ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - { - private IAccountRepository _repository; - - public ChangePasswordHandler(IAccountRepository repository) - { - _repository = repository; - } - - public async Task ExecuteAsync(ChangePassword request) - { - var user = await _repository.GetByIdAsync(request.UserId); -<<<<<<< HEAD - //if (!user.ValidatePassword(request.CurrentPassword)) - // return new ChangePasswordReply() {Success = false}; -======= - if (!user.ValidatePassword(request.CurrentPassword)) - return new ChangePasswordReply() { Success = false }; ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - user.ChangePassword(request.NewPassword); - await _repository.UpdateAsync(user); - return new ChangePasswordReply {Success = true}; - } - } -} diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/LoginHandler.cs b/src/Server/OneTrueError.App/Core/Accounts/Requests/LoginHandler.cs deleted file mode 100644 index 16c92b97..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/LoginHandler.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System; -using System.Security.Authentication; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Accounts.Requests; - -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// - [Component(Lifetime = Lifetime.Scoped)] - public class LoginHandler : IRequestHandler - { - private readonly IEventBus _eventBus; - private readonly ILog _logger = LogManager.GetLogger(typeof(LoginHandler)); - private readonly IAccountRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// used to publish . - /// repository; eventBus - public LoginHandler(IAccountRepository repository, IEventBus eventBus) - { - if (repository == null) throw new ArgumentNullException("repository"); - if (eventBus == null) throw new ArgumentNullException("eventBus"); - _repository = repository; - _eventBus = eventBus; - } - - /// - /// Execute the request and generate a reply. - /// - /// Request to execute - /// - /// Task which will contain the reply once completed. - /// - public async Task ExecuteAsync(Login request) - { - if (request == null) throw new ArgumentNullException("request"); - - var account = await _repository.FindByUserNameAsync(request.UserName); - - try - { - if (account == null || !account.Login(request.Password)) - { - _logger.Debug("Logging in " + request.UserName); - await _eventBus.PublishAsync(new LoginFailed(request.UserName) {InvalidLogin = true}); - if (account != null) - await _repository.UpdateAsync(account); - return new LoginReply {Result = LoginResult.IncorrectLogin}; - } - } - catch (AuthenticationException ex) - { - _logger.Debug("Logging failed for " + request.UserName, ex); - _eventBus.PublishAsync(new LoginFailed(request.UserName) {IsLocked = true}).Wait(); - return new LoginReply {Result = LoginResult.Locked}; - } - - await _repository.UpdateAsync(account); - return new LoginReply - { - Result = LoginResult.Successful, - UserName = account.UserName, - AccountId = account.Id - }; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/LoginHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/Requests/LoginHandler.cs.orig deleted file mode 100644 index 0c125885..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/LoginHandler.cs.orig +++ /dev/null @@ -1,94 +0,0 @@ -using System.Security.Authentication; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -<<<<<<< HEAD -using OneTrueError.AccountManagement.App.Customers; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Accounts.Requests; - -namespace OneTrueError.AccountManagement.App.Accounts.Requests -{ -======= -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Accounts.Requests; - -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - [Component] - public class LoginHandler : IRequestHandler - { - private readonly IEventBus _eventBus; - private readonly IAccountRepository _repository; -<<<<<<< HEAD - private readonly ICustomerRepository _customerRepository; - private ILog _logger = LogManager.GetLogger(typeof(LoginHandler)); - - public LoginHandler(IAccountRepository repository, ICustomerRepository customerRepository, IEventBus eventBus) - { - _repository = repository; - _customerRepository = customerRepository; -======= - private ILog _logger = LogManager.GetLogger(typeof(LoginHandler)); - - public LoginHandler(IAccountRepository repository, IEventBus eventBus) - { - _repository = repository; ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - _eventBus = eventBus; - } - - public async Task ExecuteAsync(Login request) - { - var user = await _repository.FindByUsernameAsync(request.UserName); - - try - { - if (user == null || !user.Login(request.Password)) - { - _logger.Info("Failed to find user " + request.UserName); - await _eventBus.PublishAsync(new LoginFailed(request.UserName) { InvalidLogin = true }); - _logger.Info("Replying for " + request.UserName); - return new LoginReply { Result = LoginResult.IncorrectLogin }; - } - } - catch (AuthenticationException exception) - { - _logger.Error("Could not login " + request.UserName, exception); - - //no await is intentional -#pragma warning disable 4014 - _eventBus.PublishAsync(new LoginFailed(request.UserName) { IsLocked = true }); -#pragma warning restore 4014 - - return new LoginReply { Result = LoginResult.Locked }; - } - - await _repository.LoggedInAsync(request.UserName); - _logger.Info("Logged in " + request.UserName); -<<<<<<< HEAD - var customer = await _customerRepository.GetByIdAsync(user.CustomerId); -======= ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - return new LoginReply - { - Result = LoginResult.Successful, - UserName = user.UserName, -<<<<<<< HEAD - AccountId = user.Id, - IsOnTrial = !customer.HasTrialExpired, - TrialDaysLeft = customer.DaysUntilTrialExpires, - CustomerId = customer.Id, - CustomerName = customer.Name -======= - AccountId = user.Id ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - }; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/ResetPasswordHandler.cs b/src/Server/OneTrueError.App/Core/Accounts/Requests/ResetPasswordHandler.cs deleted file mode 100644 index 0795f375..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/ResetPasswordHandler.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Requests; - -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// - [Component] - public class ResetPasswordHandler : IRequestHandler - { - private readonly IAccountRepository _accountRepository; - - /// - /// Creates a new instance of . - /// - /// accountRepository - /// accountRepository - public ResetPasswordHandler(IAccountRepository accountRepository) - { - if (accountRepository == null) throw new ArgumentNullException("accountRepository"); - _accountRepository = accountRepository; - } - - /// - /// Execute the request and generate a reply. - /// - /// Request to execute - /// - /// Task which will contain the reply once completed. - /// - public async Task ExecuteAsync(ResetPassword request) - { - var account = await _accountRepository.FindByActivationKeyAsync(request.ActivationKey); - account.ChangePassword(request.NewPassword); - await _accountRepository.UpdateAsync(account); - return new ResetPasswordReply(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/ResetPasswordHandler.cs.orig b/src/Server/OneTrueError.App/Core/Accounts/Requests/ResetPasswordHandler.cs.orig deleted file mode 100644 index 1bb61955..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/ResetPasswordHandler.cs.orig +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Requests; - -<<<<<<< HEAD -namespace OneTrueError.AccountManagement.App.Accounts.Requests -{ -======= -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - [Component] - public class ResetPasswordHandler : IRequestHandler - { - private readonly IAccountRepository _accountRepository; - - public ResetPasswordHandler(IAccountRepository accountRepository) - { - _accountRepository = accountRepository; - } - - public async Task ExecuteAsync(ResetPassword request) - { - var account = _accountRepository.FindByActivationKey(request.ActivationKey); - account.ChangePassword(request.NewPassword); - await _accountRepository.UpdateAsync(account); - return new ResetPasswordReply(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Accounts/Requests/ValidateNewLoginHandler.cs b/src/Server/OneTrueError.App/Core/Accounts/Requests/ValidateNewLoginHandler.cs deleted file mode 100644 index 7a5f47d8..00000000 --- a/src/Server/OneTrueError.App/Core/Accounts/Requests/ValidateNewLoginHandler.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Requests; - -namespace OneTrueError.App.Core.Accounts.Requests -{ - /// - /// Handler for . - /// - [Component] - public class ValidateNewLoginHandler : IRequestHandler - { - private readonly IAccountRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// repository - public ValidateNewLoginHandler(IAccountRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - /// Execute the request and generate a reply. - /// - /// Request to execute - /// - /// Task which will contain the reply once completed. - /// - public async Task ExecuteAsync(ValidateNewLogin request) - { - var reply = new ValidateNewLoginReply(); - if (!string.IsNullOrEmpty(request.Email)) - reply.EmailIsTaken = await _repository.IsEmailAddressTakenAsync(request.Email); - - if (!string.IsNullOrEmpty(request.UserName)) - reply.UserNameIsTaken = await _repository.FindByUserNameAsync(request.UserName) != null; - - return reply; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/ApiKeys/ApiKey.cs b/src/Server/OneTrueError.App/Core/ApiKeys/ApiKey.cs deleted file mode 100644 index ebf10216..00000000 --- a/src/Server/OneTrueError.App/Core/ApiKeys/ApiKey.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Security.Cryptography; -using System.Text; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.App.Core.ApiKeys -{ - /// - /// A generated API key which can be used to call OneTrueError´s HTTP api. - /// - public class ApiKey - { - private readonly List _claims = new List(); - - /// - /// Application that will be using this key - /// - public string ApplicationName { get; set; } - - - /// - /// Claims associated with this key - /// - /// - /// - /// Typically contains to identity which applications the key can access. - /// - /// - public Claim[] Claims { get; set; } - - /// - /// When this key was generated - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// AccountId that generated this key - /// - public int CreatedById { get; set; } - - /// - /// Api key - /// - public string GeneratedKey { get; set; } - - - /// - /// PK - /// - public int Id { get; set; } - - - /// - /// Used when generating signatures. - /// - public string SharedSecret { get; set; } - - /// - /// Add an application that this ApiKey can be used for. - /// - /// application id - public void Add(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - - _claims.Add(new Claim(OneTrueClaims.Application, applicationId.ToString(), ClaimValueTypes.Integer32)); - } - - /// - /// Validate a given signature using the HTTP body. - /// - /// Signature passed from the client - /// HTTP body (i.e. the data that the signature was generated on) - /// true if the signature was generated using the shared secret; otherwise false. - public bool ValidateSignature(string specifiedSignature, byte[] body) - { - var hashAlgo = new HMACSHA256(Encoding.UTF8.GetBytes(SharedSecret.ToLower())); - var hash = hashAlgo.ComputeHash(body); - var signature = Convert.ToBase64String(hash); - - var hashAlgo1 = new HMACSHA256(Encoding.UTF8.GetBytes(SharedSecret.ToUpper())); - var hash1 = hashAlgo1.ComputeHash(body); - var signature1 = Convert.ToBase64String(hash1); - - return specifiedSignature.Equals(signature) || specifiedSignature.Equals(signature1); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/ApiKeys/Events/ApplicationDeletedHandler.cs b/src/Server/OneTrueError.App/Core/ApiKeys/Events/ApplicationDeletedHandler.cs deleted file mode 100644 index 48c62123..00000000 --- a/src/Server/OneTrueError.App/Core/ApiKeys/Events/ApplicationDeletedHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Applications.Events; - -namespace OneTrueError.App.Core.ApiKeys.Events -{ - /// - /// Will either delete an entire apikey (if the only association is with the given application) or just remove the - /// application mapping. - /// - [Component(RegisterAsSelf = true)] - public class ApplicationDeletedHandler : IApplicationEventSubscriber - { - private readonly IApiKeyRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - public ApplicationDeletedHandler(IApiKeyRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - public async Task HandleAsync(ApplicationDeleted e) - { - var apps = await _repository.GetForApplicationAsync(e.ApplicationId); - foreach (var apiKey in apps) - { - if (apiKey.Claims.Length == 1) - await _repository.DeleteAsync(apiKey.Id); - else - await _repository.DeleteApplicationMappingAsync(apiKey.Id, e.ApplicationId); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/ApiKeys/ReadMe.md b/src/Server/OneTrueError.App/Core/ApiKeys/ReadMe.md deleted file mode 100644 index 72b68d79..00000000 --- a/src/Server/OneTrueError.App/Core/ApiKeys/ReadMe.md +++ /dev/null @@ -1,5 +0,0 @@ -Api Keys -========= - -Api keys are used to allow other applications to communicate with OneTrueError through the HTTP api. - diff --git a/src/Server/OneTrueError.App/Core/Applications/Application.cs b/src/Server/OneTrueError.App/Core/Applications/Application.cs deleted file mode 100644 index bd89ea3d..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/Application.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using OneTrueError.Api.Core.Applications; - -namespace OneTrueError.App.Core.Applications -{ - /// - /// An application which we can receive exceptions from. - /// - public class Application - { - /// - /// Initializes a new instance of the class. - /// - /// Account id for the user that created this application. - /// Application name as defined by the user. - public Application(int createdById, string name) - { - if (createdById < 1) throw new ArgumentNullException("createdById"); - if (name == null) throw new ArgumentNullException("name"); - - CreatedById = createdById; - AppKey = Guid.NewGuid().ToString("N"); - Name = name; - CreatedAtUtc = DateTime.UtcNow; - SharedSecret = Guid.NewGuid().ToString("N"); - } - - /// - /// Serialization constructor - /// - protected Application() - { - } - - /// - /// Gets or sets ID used to identify this application - /// - /// - /// The application id are used in the query string when reports are sent. It's then used to find the correct - /// application so that we can decrypt using the shared secret. - /// - public string AppKey { get; set; } - - /// - /// Defines the type of application - /// - /// - /// Used to configure how the analysis should be made. - /// - public TypeOfApplication ApplicationType { get; set; } - - /// - /// When the application was created. - /// - public DateTime CreatedAtUtc { get; private set; } - - /// - /// Account id for the user that created the application. - /// - public int CreatedById { get; set; } - - /// - /// Gets db identifier used in relations. - /// - // ReSharper disable once UnusedAutoPropertyAccessor.Local, set by reflection - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", - Justification = "Set by reflection.")] - // ReSharper disable once UnusedAutoPropertyAccessor.Local - public int Id { get; private set; } - - /// - /// Gets title - /// - public string Name { get; set; } - - /// - /// Gets or sets the shared secret which is used to encrypt sensitive data between the reporter client and the server. - /// - /// The user have to manually configure the secret. - public string SharedSecret { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/ApplicationMember.cs b/src/Server/OneTrueError.App/Core/Applications/ApplicationMember.cs deleted file mode 100644 index acc0d2ef..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/ApplicationMember.cs +++ /dev/null @@ -1,26 +0,0 @@ -//using System; - -//namespace OneTrueError.App.Core.Applications -//{ -// /// -// /// Someone that can see and work with all incidents for a specific application. -// /// -// public class ApplicationMember -// { -// /// -// /// If invited, but not yet accepted. -// /// -// public string EmailAddress { get; set; } - - -// /// -// /// DateTime.MinValue if invitation have been accepted. -// /// -// public DateTime InvitedAtUtc { get; set; } - -// /// -// /// User id -// /// -// public int UserId { get; set; } -// } -//} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/CreateApplicationHandler.cs b/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/CreateApplicationHandler.cs deleted file mode 100644 index 12b21975..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/CreateApplicationHandler.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Applications; -using OneTrueError.Api.Core.Applications.Commands; -using OneTrueError.Api.Core.Applications.Events; -using OneTrueError.App.Core.Users; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.App.Core.Applications.CommandHandlers -{ - [Component] - internal class CreateApplicationHandler : ICommandHandler - { - private readonly IEventBus _eventBus; - private readonly IApplicationRepository _repository; - private readonly IUserRepository _userRepository; - - public CreateApplicationHandler(IApplicationRepository repository, IUserRepository userRepository, - IEventBus eventBus) - { - _repository = repository; - _userRepository = userRepository; - _eventBus = eventBus; - } - - public async Task ExecuteAsync(CreateApplication command) - { - var app = new Application(command.UserId, command.Name) - { - AppKey = command.ApplicationKey, - ApplicationType = - (TypeOfApplication) Enum.Parse(typeof(TypeOfApplication), command.TypeOfApplication.ToString()) - }; - var creator = await _userRepository.GetUserAsync(command.UserId); - - await _repository.CreateAsync(app); - await _repository.CreateAsync(new ApplicationTeamMember(app.Id, creator.AccountId) - { - UserName = creator.UserName, - Roles = new[] {ApplicationRole.Admin, ApplicationRole.Member}, - AddedByName = creator.UserName - }); - - var identity = ClaimsPrincipal.Current.Identities.First(); - var claim = new Claim(OneTrueClaims.Application, app.Id.ToString(), ClaimValueTypes.Integer32); - identity.AddClaim(claim); - claim = new Claim(OneTrueClaims.ApplicationAdmin, app.Id.ToString(), ClaimValueTypes.Integer32); - identity.AddClaim(claim); - claim = new Claim(OneTrueClaims.ApplicationName, app.Name, ClaimValueTypes.String); - identity.AddClaim(claim); - identity.AddClaim(OneTrueClaims.UpdateIdentity); - - var evt = new ApplicationCreated(app.Id, app.Name, command.UserId, command.ApplicationKey, app.SharedSecret); - await _eventBus.PublishAsync(evt); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/DeleteApplicationHandler.cs b/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/DeleteApplicationHandler.cs deleted file mode 100644 index c2bf88a9..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/DeleteApplicationHandler.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Security.Claims; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Applications.Commands; -using OneTrueError.Api.Core.Applications.Events; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.App.Core.Applications.CommandHandlers -{ - /// - /// Handler for . - /// - [Component(RegisterAsSelf = true)] - public class DeleteApplicationHandler : ICommandHandler - { - private readonly IEventBus _eventBus; - private readonly IApplicationRepository _repository; - - /// - /// Creates a new instance of . - /// - /// used to delete the application - /// to publish ApplicationDeleted - public DeleteApplicationHandler(IApplicationRepository repository, IEventBus eventBus) - { - _repository = repository; - _eventBus = eventBus; - } - - /// - public async Task ExecuteAsync(DeleteApplication command) - { - ClaimsPrincipal.Current.EnsureApplicationAdmin(command.Id); - - var app = await _repository.GetByIdAsync(command.Id); - await _repository.DeleteAsync(command.Id); - var evt = new ApplicationDeleted {ApplicationName = app.Name, ApplicationId = app.Id, AppKey = app.AppKey}; - await _eventBus.PublishAsync(evt); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/RemoveTeamMemberHandler.cs b/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/RemoveTeamMemberHandler.cs deleted file mode 100644 index 05c8b25b..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/RemoveTeamMemberHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Security.Claims; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Applications.Commands; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.App.Core.Applications.CommandHandlers -{ - /// - /// Remove a team member from an application - /// - [Component] - public class RemoveTeamMemberHandler : ICommandHandler - { - private readonly IApplicationRepository _applicationRepository; - private readonly ILog _logger = LogManager.GetLogger(typeof(RemoveTeamMember)); - - /// - /// Creates a new instance of . - /// - /// To remove member - public RemoveTeamMemberHandler(IApplicationRepository applicationRepository) - { - _applicationRepository = applicationRepository; - } - - /// - public async Task ExecuteAsync(RemoveTeamMember command) - { - ClaimsPrincipal.Current.EnsureApplicationAdmin(command.ApplicationId); - await _applicationRepository.RemoveTeamMemberAsync(command.ApplicationId, command.UserToRemove); - _logger.Info("Removed " + command.UserToRemove + " from application " + command.ApplicationId); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/UpdateApplicationHandler.cs b/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/UpdateApplicationHandler.cs deleted file mode 100644 index 335ebba8..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/CommandHandlers/UpdateApplicationHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Applications.Commands; - -namespace OneTrueError.App.Core.Applications.CommandHandlers -{ - /// - /// Used to update application name and applicationType. - /// - [Component] - public class UpdateApplicationHandler : ICommandHandler - { - private readonly IApplicationRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - public UpdateApplicationHandler(IApplicationRepository repository) - { - _repository = repository; - } - - /// - public async Task ExecuteAsync(UpdateApplication command) - { - var app = await _repository.GetByIdAsync(command.ApplicationId); - app.Name = command.Name; - if (command.TypeOfApplication != null) - app.ApplicationType = command.TypeOfApplication.Value; - await _repository.UpdateAsync(app); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/EventHandlers/UpdateTeamOnInvitationAccepted.cs b/src/Server/OneTrueError.App/Core/Applications/EventHandlers/UpdateTeamOnInvitationAccepted.cs deleted file mode 100644 index 77ced8ff..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/EventHandlers/UpdateTeamOnInvitationAccepted.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Applications.Events.OneTrueError.Api.Core.Accounts.Events; - -namespace OneTrueError.App.Core.Applications.EventHandlers -{ - [Component(RegisterAsSelf = true)] - internal class UpdateTeamOnInvitationAccepted : IApplicationEventSubscriber - { - private readonly IApplicationRepository _applicationRepository; - private readonly IEventBus _eventBus; - - public UpdateTeamOnInvitationAccepted(IApplicationRepository applicationRepository, IEventBus eventBus) - { - _applicationRepository = applicationRepository; - _eventBus = eventBus; - } - - public async Task HandleAsync(InvitationAccepted e) - { - foreach (var applicationId in e.ApplicationIds) - { - var members = await _applicationRepository.GetTeamMembersAsync(applicationId); - var member = members.FirstOrDefault(x => x.EmailAddress == e.InvitedEmailAddress && x.AccountId == 0); - if (member != null) - { - member.AcceptInvitation(e.AccountId); - await _applicationRepository.UpdateAsync(member); - await _eventBus.PublishAsync(new UserAddedToApplication(applicationId, e.AccountId)); - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetApplicationInfoHandler.cs b/src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetApplicationInfoHandler.cs deleted file mode 100644 index 2417c3a8..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetApplicationInfoHandler.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core; -using OneTrueError.Api.Core.Applications; -using OneTrueError.Api.Core.Applications.Queries; -using OneTrueError.App.Core.Incidents; - -namespace OneTrueError.App.Core.Applications.QueryHandlers -{ - /// - /// Handler for . - /// - [Component] - public class GetApplicationInfoHandler : IQueryHandler - { - private readonly IIncidentRepository _incidentRepository; - private readonly IApplicationRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// used to count the number of incidents - public GetApplicationInfoHandler(IApplicationRepository repository, IIncidentRepository incidentRepository) - { - if (repository == null) throw new ArgumentNullException("repository"); - if (incidentRepository == null) throw new ArgumentNullException("incidentRepository"); - _repository = repository; - _incidentRepository = incidentRepository; - } - - /// - /// Method used to execute the query - /// - /// Query to execute. - /// - /// Task which will contain the result once completed. - /// - public async Task ExecuteAsync(GetApplicationInfo query) - { - Application app; - if (!string.IsNullOrEmpty(query.AppKey)) - { - app = await _repository.GetByKeyAsync(query.AppKey); - } - else - { - app = await _repository.GetByIdAsync(query.ApplicationId); - } - - var totalCount = await _incidentRepository.GetTotalCountForAppInfoAsync(app.Id); - - return new GetApplicationInfoResult - { - AppKey = app.AppKey, - ApplicationType = app.ApplicationType.ConvertEnum(), - Id = app.Id, - Name = app.Name, - SharedSecret = app.SharedSecret, - TotalIncidentCount = totalCount - }; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetMyApplications.cs b/src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetMyApplications.cs deleted file mode 100644 index 87f3357e..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/QueryHandlers/GetMyApplications.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Applications; -using OneTrueError.Api.Core.Applications.Queries; -using OneTrueError.App.Core.Accounts; - -namespace OneTrueError.App.Core.Applications.QueryHandlers -{ - /// - /// Handler for . - /// - [Component] - public class GetApplicationListHandler : IQueryHandler - { - private readonly IApplicationRepository _applicationRepository; - private readonly IAccountRepository _accountRepository; - - /// - /// Creates a new instance of . - /// - /// repos - /// used to check if the user is sysadmin (can get all applications) - public GetApplicationListHandler(IApplicationRepository applicationRepository, - IAccountRepository accountRepository) - { - if (applicationRepository == null) throw new ArgumentNullException("applicationRepository"); - _applicationRepository = applicationRepository; - _accountRepository = accountRepository; - } - - /// - /// Method used to execute the query - /// - /// Query to execute. - /// - /// Task which will contain the result once completed. - /// - public async Task ExecuteAsync(GetApplicationList query) - { - if (query == null) throw new ArgumentNullException("query"); - ApplicationListItem[] result; - - if (query.AccountId > 0) - { - var account = await _accountRepository.GetByIdAsync(query.AccountId); - if (account.IsSysAdmin) - query.AccountId = 0; - } - - if (query.AccountId != 0) - { - var apps = await _applicationRepository.GetForUserAsync(query.AccountId); - result = ( - from x in apps - select new ApplicationListItem(x.ApplicationId, x.ApplicationName) {IsAdmin = x.IsAdmin} - ).ToArray(); - } - else - result = (await _applicationRepository.GetAllAsync()) - .Select(x => new ApplicationListItem(x.Id, x.Name)) - .ToArray(); - - return result; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Applications/UserApplication.cs b/src/Server/OneTrueError.App/Core/Applications/UserApplication.cs deleted file mode 100644 index 6bb7665c..00000000 --- a/src/Server/OneTrueError.App/Core/Applications/UserApplication.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OneTrueError.App.Core.Applications -{ - /// - /// Application that a specific user is a member of - /// - public class UserApplication - { - /// - /// App name - /// - public string ApplicationName { get; set; } - - /// - /// ID - /// - public int ApplicationId { get; set; } - - /// - /// If the user that this app is requested for is the admin - /// - public bool IsAdmin { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Feedback/EventSubscribers/AttachFeedbackToIncident.cs b/src/Server/OneTrueError.App/Core/Feedback/EventSubscribers/AttachFeedbackToIncident.cs deleted file mode 100644 index 97d7bd71..00000000 --- a/src/Server/OneTrueError.App/Core/Feedback/EventSubscribers/AttachFeedbackToIncident.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Feedback.Events; -using OneTrueError.Api.Core.Incidents.Events; - -namespace OneTrueError.App.Core.Feedback.EventSubscribers -{ - /// - /// Responsible of attaching feedback to incidents when the feedback was uploaded before the actual incident. - /// - [Component(RegisterAsSelf = true)] - internal class AttachFeedbackToIncident : IApplicationEventSubscriber - { - private readonly IEventBus _eventBus; - private readonly IFeedbackRepository _repository; - - public AttachFeedbackToIncident(IFeedbackRepository repository, IEventBus eventBus) - { - if (eventBus == null) throw new ArgumentNullException("eventBus"); - _repository = repository; - _eventBus = eventBus; - } - - public async Task HandleAsync(ReportAddedToIncident e) - { - var feedback = await _repository.FindPendingAsync(e.Report.ReportId); - if (feedback == null) - return; - - feedback.AssignToReport(e.Report.Id, e.Incident.Id, e.Incident.ApplicationId); - - var evt = new FeedbackAttachedToIncident - { - IncidentId = e.Incident.Id, - Message = feedback.Description, - UserEmailAddress = feedback.EmailAddress - }; - await _eventBus.PublishAsync(evt); - await _repository.UpdateAsync(feedback); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Feedback/EventSubscribers/StoreFeedbackFromNewReports.cs b/src/Server/OneTrueError.App/Core/Feedback/EventSubscribers/StoreFeedbackFromNewReports.cs deleted file mode 100644 index 0ea60dcc..00000000 --- a/src/Server/OneTrueError.App/Core/Feedback/EventSubscribers/StoreFeedbackFromNewReports.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Feedback.Commands; -using OneTrueError.Api.Core.Incidents.Events; - -namespace OneTrueError.App.Core.Feedback.EventSubscribers -{ - /// - /// Responsible of separating the feedback from the incident when it's uploaded as context data. - /// - [Component(RegisterAsSelf = true)] - public class StoreFeedbackFromNewReports : IApplicationEventSubscriber - { - private readonly ICommandBus _commandBus; - private readonly ILog _logger = LogManager.GetLogger(typeof(StoreFeedbackFromNewReports)); - - /// - /// Creates a new instance of . - /// - /// to send the command - /// commandBus - public StoreFeedbackFromNewReports(ICommandBus commandBus) - { - if (commandBus == null) throw new ArgumentNullException("commandBus"); - _commandBus = commandBus; - } - - /// - /// Process an event asynchronously. - /// - /// event to process - /// - /// Task to wait on. - /// - public async Task HandleAsync(ReportAddedToIncident e) - { - try - { - _logger.Debug("storing feedback for report " + e.Report.ReportId); - var userInfo = e.Report.ContextCollections.FirstOrDefault(x => x.Name == "UserSuppliedInformation"); - if (userInfo == null) - return; - - string description; - string email; - userInfo.Properties.TryGetValue("Description", out description); - userInfo.Properties.TryGetValue("Email", out email); - - var cmd = new SubmitFeedback(e.Report.ReportId, e.Report.RemoteAddress ?? "") - { - Feedback = description, - Email = email - }; - - await _commandBus.ExecuteAsync(cmd); - } - catch (Exception exception) - { - _logger.Error("Failed for " + e.Report.ReportId, exception); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Feedback/FeedbackEntity.cs b/src/Server/OneTrueError.App/Core/Feedback/FeedbackEntity.cs deleted file mode 100644 index 6edd98d4..00000000 --- a/src/Server/OneTrueError.App/Core/Feedback/FeedbackEntity.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; - -namespace OneTrueError.App.Core.Feedback -{ - /// - /// Feedback written by the user when an exception was thrown - /// - public class FeedbackEntity - { - /// - /// Application that the feedback is for - /// - public int ApplicationId { get; private set; } - - /// - /// Feedback entry can be removed. - /// - /// - /// - /// We can receive feedback before the exception have been uploaded. In those situations we need to wait on the - /// error report - /// before we know what incident the feedback belongs to. But since we do not want a lot of junk in our tables we - /// keep - /// unidentified feedback entries just for a couple of days. - /// - /// - public bool CanRemove - { - get { return ApplicationId == 0 && DateTime.Now.Subtract(CreatedAtUtc).TotalDays > 5; } - } - - - /// - /// Can only update the entry if we've been associated to a report+application. - /// - public bool CanUpdate - { - get - { - return ApplicationId != 0 - || ReportId != 0; - } - } - - /// - /// When the feebback was created by the client library - /// - public DateTime CreatedAtUtc { get; private set; } - - /// - /// Description written by the user (of what he/she did when the exception was created). - /// - public string Description { get; private set; } - - /// - /// Email address if the user want to get notified of progress. - /// - public string EmailAddress { get; private set; } - - /// - /// The unique error id that was generated by the client library - /// - public string ErrorId { get; private set; } - - /// - /// PK - /// - public int Id { get; set; } - - /// - /// Incident that the feedback was created for - /// - public int IncidentId { get; private set; } - - /// - /// PK for the report in our DB - /// - public int ReportId { get; private set; } - - /// - /// We've identified which report this feedback belongs to - /// - /// Report PK - /// Incident that the report belongs to - /// Application that the incident belongs to - public void AssignToReport(int reportId, int incidentId, int applicationId) - { - if (reportId <= 0) throw new ArgumentOutOfRangeException("reportId"); - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - - ReportId = reportId; - IncidentId = incidentId; - ApplicationId = applicationId; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Feedback/InvalidErrorReport.cs b/src/Server/OneTrueError.App/Core/Feedback/InvalidErrorReport.cs deleted file mode 100644 index 19257fb7..00000000 --- a/src/Server/OneTrueError.App/Core/Feedback/InvalidErrorReport.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.App.Core.Feedback -{ - /// - /// We've failed to save this report. - /// - public class InvalidErrorReport - { - /// - /// Creates a new instance of . - /// - /// application id - /// applicationId - public InvalidErrorReport(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - AddedAtUtc = DateTime.UtcNow; - } - - /// - /// When this report was uploaded. - /// - public DateTime AddedAtUtc { get; private set; } - - /// - /// Application id - /// - public int ApplicationId { get; private set; } - - /// - /// Why the report receiving failed. - /// - public string Exception { get; set; } - - /// - /// Invalid report id - /// - public int Id { get; private set; } - - /// - /// Raw report bytes (i.e. not deserialized) - /// - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", - Justification = "I like my arrays.")] - public byte[] Report { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Incidents/Commands/CloseIncidentHandler.cs b/src/Server/OneTrueError.App/Core/Incidents/Commands/CloseIncidentHandler.cs deleted file mode 100644 index b1614d8e..00000000 --- a/src/Server/OneTrueError.App/Core/Incidents/Commands/CloseIncidentHandler.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Incidents.Commands; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Core.Feedback; - -namespace OneTrueError.App.Core.Incidents.Commands -{ - /// - /// Handler of . - /// - [Component] - public class CloseIncidentHandler : ICommandHandler - { - private readonly ICommandBus _commandBus; - private readonly IFeedbackRepository _feedbackRepository; - private readonly IIncidentRepository _repository; - - /// - /// Creates a new instance of . - /// - /// To be able to load and update incident - /// To be able to see if someone is waiting on update notifications. - /// To send notification emails. - public CloseIncidentHandler(IIncidentRepository repository, IFeedbackRepository feedbackRepository, - ICommandBus commandBus) - { - _repository = repository; - _feedbackRepository = feedbackRepository; - _commandBus = commandBus; - } - - /// - /// Execute a command asynchronously. - /// - /// Command to execute. - /// - /// Task which will be completed once the command has been executed. - /// - public async Task ExecuteAsync(CloseIncident command) - { - if (command == null) throw new ArgumentNullException("command"); - - var incident = await _repository.GetAsync(command.IncidentId); - incident.Solve(command.UserId, command.Solution); - if (command.ShareSolution) - incident.ShareSolution(); - - if (command.CanSendNotification && !string.IsNullOrEmpty(command.NotificationTitle) && - !string.IsNullOrEmpty(command.NotificationText)) - { - var emails = await _feedbackRepository.GetEmailAddressesAsync(command.IncidentId); - var emailMessage = new EmailMessage(emails) - { - Subject = command.NotificationTitle, - TextBody = command.NotificationText - }; - var sendMessage = new SendEmail(emailMessage); - await _commandBus.ExecuteAsync(sendMessage); - } - - //var reports = _reportsRepository.GetAll(incident.Reports.Select(x => x.ReportId).ToArray()); - //foreach (var report in reports) - //{ - // report.Solve(command.Solution); - // _reportsRepository.Update(report); - //} - - await _repository.UpdateAsync(incident); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Incidents/Commands/IgnoreIncidentHandler.cs b/src/Server/OneTrueError.App/Core/Incidents/Commands/IgnoreIncidentHandler.cs deleted file mode 100644 index 0d0d663d..00000000 --- a/src/Server/OneTrueError.App/Core/Incidents/Commands/IgnoreIncidentHandler.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Incidents.Commands; -using OneTrueError.Api.Core.Incidents.Events; -using OneTrueError.App.Core.Users; - -namespace OneTrueError.App.Core.Incidents.Commands -{ - /// - /// Handler for . - /// - [Component] - public class IgnoreIncidentHandler : ICommandHandler - { - private readonly IEventBus _eventBus; - private readonly IIncidentRepository _incidentRepository; - private readonly IUserRepository _userRepository; - - /// - /// Creates a new instance of . - /// - /// to load and update info about the incident that is being ignored - /// to get info about the user that ignores the incident - /// to publish ignore event - /// - public IgnoreIncidentHandler(IIncidentRepository incidentRepository, IUserRepository userRepository, - IEventBus eventBus) - { - if (incidentRepository == null) throw new ArgumentNullException("incidentRepository"); - if (userRepository == null) throw new ArgumentNullException("userRepository"); - if (eventBus == null) throw new ArgumentNullException("eventBus"); - - _incidentRepository = incidentRepository; - _userRepository = userRepository; - _eventBus = eventBus; - } - - /// Execute a command asynchronously. - /// Command to execute. - /// Task which will be completed once the command has been executed. - public async Task ExecuteAsync(IgnoreIncident command) - { - var user = await _userRepository.GetUserAsync(command.UserId); - var incident = await _incidentRepository.GetAsync(command.IncidentId); - incident.IgnoreFutureReports(user.UserName); - await _incidentRepository.UpdateAsync(incident); - - await _eventBus.PublishAsync(new IncidentIgnored(command.IncidentId, user.AccountId, user.UserName)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Incidents/IIncidentRepository.cs b/src/Server/OneTrueError.App/Core/Incidents/IIncidentRepository.cs deleted file mode 100644 index 649ea08b..00000000 --- a/src/Server/OneTrueError.App/Core/Incidents/IIncidentRepository.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Threading.Tasks; -using Griffin.Data; - -namespace OneTrueError.App.Core.Incidents -{ - /// - /// Incident repository - /// - public interface IIncidentRepository - { - /// - /// Get incident - /// - /// incident id - /// incident - /// id - /// No incident was found with the given key. - Task GetAsync(int id); - - /// - /// Count the number of incidents for the given application - /// - /// application - /// total count of incidents - /// applicationId - Task GetTotalCountForAppInfoAsync(int applicationId); - - /// - /// Update incident - /// - /// incdient - /// task - /// incident - Task UpdateAsync(Incident incident); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Incidents/Incident.cs b/src/Server/OneTrueError.App/Core/Incidents/Incident.cs deleted file mode 100644 index c9f617db..00000000 --- a/src/Server/OneTrueError.App/Core/Incidents/Incident.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.App.Core.Incidents -{ - /// - /// Keeps track of all occurrences of a single incident (i.e. error reports which generates the same hash code) - /// - public class Incident - { - private string _description; - - /// - /// Serialization constructor - /// - protected Incident() - { - } - - /// - /// Creates a new instance of . - /// - /// application that the incident was created for - /// applicationId - public Incident(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - - ApplicationId = applicationId; - CreatedAtUtc = DateTime.UtcNow; - } - - /// - /// Application that this incident belongs to - /// - public int ApplicationId { get; private set; } - - /// - /// When the incident was created in the client library. - /// - public DateTime CreatedAtUtc { get; private set; } - - /// - /// Incident description.Typically first line of the exception message. - /// - public string Description - { - get - { - if (string.IsNullOrEmpty(_description)) - return "Ooops Error!"; - - return _description; - } - set { _description = value; } - } - - /// - /// PK - /// - public int Id { get; private set; } - - /// - /// Do not accept any more reports for this exception - /// - /// - /// - /// Means that no notifications or reports should be saved on this incident - /// - /// - public bool IgnoreReports { get; private set; } - - /// - /// was set to true at this time. - /// - public DateTime IgnoringReportsSinceUtc { get; private set; } - - /// - /// Person that wanted us to ignore reports. - /// - public string IgnoringRequestedBy { get; private set; } - - /// - /// Incident was marked as completed, but we've received another report for this incident - /// - /// - /// - /// Do not apply to ignored incidents. - /// - /// - [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re")] - public bool IsReopened { get; private set; } - - /// - /// If the solution can be shared with others. - /// - public bool IsSolutionShared { get; private set; } - - /// - /// If this incident has been fixed. - /// - public bool IsSolved { get; private set; } - - /// - /// has been set to true, this tells when the incident was closed the last time. - /// - public DateTime LastSolutionAtUtc { get; private set; } - - /// - /// When it was reopened. - /// - /// - [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re")] - public DateTime ReopenedAtUtc { get; private set; } - - /// - /// Number of reports that have been received so far (counts up even if the max number of reports have been received - /// for this incident). - /// - public int ReportCount { get; set; } - - /// - /// Gets what was done to fix this error. - /// - public IncidentSolution Solution { get; private set; } - - /// - /// When the was written- - /// - public DateTime SolvedAtUtc { get; private set; } - - /// - /// Stack trace from exception. - /// - public string StackTrace { get; set; } - - /// - /// When the incident was updated through the UI or when a new report was received (whatever change was made last - /// time). - /// - public DateTime UpdatedAtUtc { get; private set; } - - /// - /// Do not want to store reports or receive notifications for this incident. - /// - /// Name of the account. - /// accountName - public void IgnoreFutureReports(string accountName) - { - if (accountName == null) throw new ArgumentNullException("accountName"); - IgnoreReports = true; - IgnoringReportsSinceUtc = DateTime.UtcNow; - IgnoringRequestedBy = accountName; - } - - /// - /// Dang! Got a new report after the incident being closed. - /// - [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re")] - public void Reopen() - { - LastSolutionAtUtc = SolvedAtUtc; - IsSolved = false; - ReopenedAtUtc = DateTime.UtcNow; - IsReopened = true; - } - - /// - /// Specifies that this solution can be shared with other projects. - /// - public void ShareSolution() - { - //TODO: Do something - IsSolutionShared = true; - } - - /// - /// Yay! One in the dev team figured out how the error can be solved. - /// - /// AccountId for whoever wrote the solution - /// Actual solution - /// solution - /// solvedBy - public void Solve(int solvedBy, string solution) - { - if (solution == null) throw new ArgumentNullException("solution"); - if (solvedBy <= 0) throw new ArgumentOutOfRangeException("solvedBy"); - - Solution = new IncidentSolution(solvedBy, solution); - UpdatedAtUtc = DateTime.UtcNow; - SolvedAtUtc = DateTime.UtcNow; - IsSolved = true; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Incidents/Jobs/DeleteEmptyIncidents.cs b/src/Server/OneTrueError.App/Core/Incidents/Jobs/DeleteEmptyIncidents.cs deleted file mode 100644 index 5939c9b2..00000000 --- a/src/Server/OneTrueError.App/Core/Incidents/Jobs/DeleteEmptyIncidents.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Threading.Tasks; -using Griffin.ApplicationServices; -using Griffin.Container; -using Griffin.Data; -using log4net; - -namespace OneTrueError.App.Core.Incidents.Jobs -{ - /// - /// Delete incidents where all reports have been deleted (due to retention days). - /// - /// - /// - /// There are other jobs where old reports are removed. This job is to make sure that old incidents are being - /// deleted - /// when there are no reports for them. Do note that ignored incidents will not be deleted. - /// - /// - [Component(RegisterAsSelf = true)] - internal class DeleteEmptyIncidents : IBackgroundJobAsync - { - private readonly ILog _logger = LogManager.GetLogger(typeof(DeleteEmptyIncidents)); - private readonly IAdoNetUnitOfWork _unitOfWork; - - /// - /// Creates a new instance of . - /// - /// Used for SQL queries - public DeleteEmptyIncidents(IAdoNetUnitOfWork unitOfWork) - { - if (unitOfWork == null) throw new ArgumentNullException("unitOfWork"); - _unitOfWork = unitOfWork; - } - - /// - public async Task ExecuteAsync() - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = - @"DELETE TOP(1000) Incidents WHERE Id IN (select Incidents.Id - FROM Incidents - LEFT JOIN ErrorReports ON (ErrorReports.IncidentId = Incidents.Id) - WHERE ErrorReports.Id IS NULL) AND IgnoreReports <> 1"; - var rows = await cmd.ExecuteNonQueryAsync(); - if (rows > 0) - { - _logger.Debug("Deleted " + rows + " empty incidents."); - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs b/src/Server/OneTrueError.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs deleted file mode 100644 index c8748c59..00000000 --- a/src/Server/OneTrueError.App/Core/Invitations/CommandHandlers/AcceptInvitationHandler.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Linq; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Accounts.Requests; -using OneTrueError.App.Core.Accounts; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.App.Core.Invitations.CommandHandlers -{ - /// - /// Accepts and deletes the invitation. Sends an event which is picked up by the application domain (which transforms - /// the pending invite to a membership) - /// - /// - /// - /// Do note that an invitation can be accepted by using another email address than the one that the invitation was - /// sent to. So take care - /// when handling the event. Update the email that as used when sending the - /// invitation. - /// - /// - [Component, UpdatesLoggedInAccount] - public class AcceptInvitationHandler : IRequestHandler - { - private readonly IAccountRepository _accountRepository; - private readonly IEventBus _eventBus; - private readonly ILog _logger = LogManager.GetLogger(typeof(AcceptInvitationHandler)); - private readonly IInvitationRepository _repository; - - /// - /// Creates a new instance of . - /// - /// invitation repos - /// To load inviter and invitee - /// to publish - public AcceptInvitationHandler(IInvitationRepository repository, - IAccountRepository accountRepository, IEventBus eventBus) - { - _repository = repository; - _accountRepository = accountRepository; - _eventBus = eventBus; - } - - /// - public async Task ExecuteAsync(AcceptInvitation request) - { - var invitation = await _repository.GetByInvitationKeyAsync(request.InvitationKey); - if (invitation == null) - { - _logger.Error("Failed to find invitation key" + request.InvitationKey); - return null; - } - await _repository.DeleteAsync(request.InvitationKey); - - Account account; - if (request.AccountId == 0) - { - account = new Account(request.UserName, request.Password); - account.SetVerifiedEmail(request.AcceptedEmail); - account.Activate(); - account.Login(request.Password); - await _accountRepository.CreateAsync(account); - } - else - { - account = await _accountRepository.GetByIdAsync(request.AccountId); - account.SetVerifiedEmail(request.AcceptedEmail); - } - - var inviter = await _accountRepository.FindByUserNameAsync(invitation.InvitedBy); - - if (ClaimsPrincipal.Current.IsAccount(account.Id)) - { - var claims = invitation.Invitations - .Select( - x => new Claim(OneTrueClaims.Application, x.ApplicationId.ToString(), ClaimValueTypes.Integer32)) - .ToList(); - - var context = new PrincipalFactoryContext(account.Id, account.UserName, new string[0]) - { - Claims = claims.ToArray(), - AuthenticationType = "Invite" - }; - var principal = await PrincipalFactory.CreateAsync(context); - principal.AddUpdateCredentialClaim(); - Thread.CurrentPrincipal = principal; - } - - // Account have not been created before the invitation was accepted. - if (request.AccountId == 0) - { - await _eventBus.PublishAsync(new AccountRegistered(account.Id, account.UserName)); - await _eventBus.PublishAsync(new AccountActivated(account.Id, account.UserName) - { - EmailAddress = account.Email - }); - } - - var e = new InvitationAccepted(account.Id, invitation.InvitedBy, account.UserName) - { - InvitedEmailAddress = invitation.EmailToInvitedUser, - AcceptedEmailAddress = request.AcceptedEmail, - ApplicationIds = invitation.Invitations.Select(x => x.ApplicationId).ToArray() - }; - await _eventBus.PublishAsync(e); - - return new AcceptInvitationReply(account.Id, account.UserName); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs b/src/Server/OneTrueError.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs deleted file mode 100644 index aa105791..00000000 --- a/src/Server/OneTrueError.App/Core/Invitations/CommandHandlers/InviteUserHandler.cs +++ /dev/null @@ -1,163 +0,0 @@ -using System.Linq; -using System.Security; -using System.Security.Claims; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Applications.Events; -using OneTrueError.Api.Core.Applications.Events.OneTrueError.Api.Core.Accounts.Events; -using OneTrueError.Api.Core.Invitations.Commands; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Configuration; -using OneTrueError.App.Core.Applications; -using OneTrueError.App.Core.Users; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.App.Core.Invitations.CommandHandlers -{ - /// - /// Handler for - /// - /// - /// - /// - /// - /// - [Component] - public class InviteUserHandler : ICommandHandler - { - private readonly IApplicationRepository _applicationRepository; - private readonly ICommandBus _commandBus; - private readonly IEventBus _eventBus; - private readonly IInvitationRepository _invitationRepository; - private readonly IUserRepository _userRepository; - private readonly ILog _logger = LogManager.GetLogger(typeof(InviteUserHandler)); - - /// - /// Creates a new instance of . - /// - /// Store invitations - /// publish invite events - /// To load inviter and invitee - /// Add pending member - /// To send in invitation email - public InviteUserHandler(IInvitationRepository invitationRepository, IEventBus eventBus, - IUserRepository userRepository, IApplicationRepository applicationRepository, ICommandBus commandBus) - { - _invitationRepository = invitationRepository; - _eventBus = eventBus; - _userRepository = userRepository; - _applicationRepository = applicationRepository; - _commandBus = commandBus; - } - - /// - public async Task ExecuteAsync(InviteUser command) - { - var inviter = await _userRepository.GetUserAsync(command.UserId); - if (!ClaimsPrincipal.Current.IsSysAdmin() && - !ClaimsPrincipal.Current.IsApplicationAdmin(command.ApplicationId)) - { - _logger.Warn($"User {command.UserId} attempted to do an invite for an application: {command.ApplicationId}."); - throw new SecurityException("You are not an admin of that application."); - } - - var invitedUser = await _userRepository.FindByEmailAsync(command.EmailAddress); - if (invitedUser != null) - { - //correction of issue #21, verify that the person isn't already a member. - var members = await _applicationRepository.GetTeamMembersAsync(command.ApplicationId); - if (members.Any(x => x.AccountId == invitedUser.AccountId)) - { - _logger.Warn("User " + invitedUser.AccountId + " is already a member."); - return; - } - - var member = new ApplicationTeamMember(command.ApplicationId, invitedUser.AccountId) - { - AddedByName = inviter.UserName, - Roles = new[] {ApplicationRole.Member} - }; - - await _applicationRepository.CreateAsync(member); - await _eventBus.PublishAsync(new UserAddedToApplication(command.ApplicationId, member.AccountId)); - return; - } - else - { - //correction of issue #21, verify that the person isn't already a member. - var members = await _applicationRepository.GetTeamMembersAsync(command.ApplicationId); - if (members.Any(x => x.EmailAddress == command.EmailAddress)) - { - _logger.Warn("User " + command.EmailAddress + " is already invited."); - return; - } - } - - var invitedMember = new ApplicationTeamMember(command.ApplicationId, command.EmailAddress) - { - AddedByName = inviter.UserName, - Roles = new[] {ApplicationRole.Member} - }; - await _applicationRepository.CreateAsync(invitedMember); - var invitation = await _invitationRepository.FindByEmailAsync(command.EmailAddress); - if (invitation == null) - { - invitation = new Invitation(command.EmailAddress, inviter.UserName); - await _invitationRepository.CreateAsync(invitation); - await SendInvitationEmailAsync(invitation, command.Text); - } - - invitation.Add(command.ApplicationId, inviter.UserName); - await _invitationRepository.UpdateAsync(invitation); - - var app = await _applicationRepository.GetByIdAsync(command.ApplicationId); - var evt = new UserInvitedToApplication( - invitation.InvitationKey, - command.ApplicationId, - app.Name, - command.EmailAddress, - inviter.UserName); - - await _eventBus.PublishAsync(evt); - } - - /// - /// Send invitation email - /// - /// Invitation to generate an email for - /// Why the user was invited (optional) - /// task - protected virtual async Task SendInvitationEmailAsync(Invitation invitation, string reason) - { - var config = ConfigurationStore.Instance.Load(); - var url = config.BaseUrl.ToString().TrimEnd('/'); - if (string.IsNullOrEmpty(reason)) - reason = ""; - else - reason += "\r\n"; - - var msg = new EmailMessage - { - Subject = "You have been invited by " + invitation.InvitedBy + " to OneTrueError.", - TextBody = string.Format(@"Hello, - -{0} has invited to you join their team at OneTrueError, a service used to keep track of exceptions in .NET applications. - -Click on the following link to accept the invitation: -{2}/account/accept/{1} - -{3} -Best regards, - The OneTrueError team -", invitation.InvitedBy, invitation.InvitationKey, url, reason), - Recipients = new[] {new EmailAddress(invitation.EmailToInvitedUser)} - }; - - await _commandBus.ExecuteAsync(new SendEmail(msg)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/ApplicationDeletedHandler.cs b/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/ApplicationDeletedHandler.cs deleted file mode 100644 index feb81ddc..00000000 --- a/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/ApplicationDeletedHandler.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using OneTrueError.Api.Core.Applications.Events; - -namespace OneTrueError.App.Core.Notifications.EventHandlers -{ - /// - /// Will delete all reports for the given application - /// - [Component(RegisterAsSelf = true)] - public class ApplicationDeletedHandler : IApplicationEventSubscriber - { - private IAdoNetUnitOfWork _uow; - - /// - /// Creates a new instance of . - /// - public ApplicationDeletedHandler(IAdoNetUnitOfWork uow) - { - if (uow == null) throw new ArgumentNullException("uow"); - _uow = uow; - } - - /// - public Task HandleAsync(ApplicationDeleted e) - { - _uow.ExecuteNonQuery("DELETE FROM UserNotificationSettings WHERE ApplicationId = @id", new { id = e.ApplicationId }); - return Task.FromResult(null); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/CheckForFeedbackNotificationsToSend.cs b/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/CheckForFeedbackNotificationsToSend.cs deleted file mode 100644 index 81dae98a..00000000 --- a/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/CheckForFeedbackNotificationsToSend.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Queries; -using OneTrueError.Api.Core.Feedback.Events; -using OneTrueError.Api.Core.Incidents.Queries; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Notifications.EventHandlers -{ - /// - /// Responsible of sending notifications when a new report have been analyzed. - /// - [Component(RegisterAsSelf = true)] - public class CheckForFeedbackNotificationsToSend : - IApplicationEventSubscriber - { - private readonly ICommandBus _commandBus; - private readonly INotificationsRepository _notificationsRepository; - private readonly IQueryBus _queryBus; - - /// - /// Creates a new instance of . - /// - /// To load notification configuration - /// To send emails - /// - public CheckForFeedbackNotificationsToSend(INotificationsRepository notificationsRepository, - ICommandBus commandBus, - IQueryBus queryBus) - { - _notificationsRepository = notificationsRepository; - _commandBus = commandBus; - _queryBus = queryBus; - } - - /// - public async Task HandleAsync(FeedbackAttachedToIncident e) - { - var settings = await _notificationsRepository.GetAllAsync(-1); - var incident = await _queryBus.QueryAsync(new GetIncident(e.IncidentId)); - foreach (var setting in settings) - { - if (setting.UserFeedback == NotificationState.Disabled) - continue; - - var notificationEmail = await _queryBus.QueryAsync(new GetAccountEmailById(setting.AccountId)); - var config = ConfigurationStore.Instance.Load(); - - var shortName = incident.Description.Length > 40 - ? incident.Description.Substring(0, 40) + "..." - : incident.Description; - - if (string.IsNullOrEmpty(e.UserEmailAddress)) - e.UserEmailAddress = "unknown"; - - var incidentUrl = string.Format("{0}/#/application/{1}/incident/{2}", - config.BaseUrl.ToString().TrimEnd('/'), - incident.ApplicationId, - incident.Id); - - //TODO: Add more information - var msg = new EmailMessage(notificationEmail); - msg.Subject = "New feedback: " + shortName; - msg.TextBody = string.Format(@"Incident: {0} -Feedback: {0}/feedback -From: {1} - -{2} -", incidentUrl, e.UserEmailAddress, e.Message); - - - var emailCmd = new SendEmail(msg); - await _commandBus.ExecuteAsync(emailCmd); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/CheckForNotificationsToSend.cs b/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/CheckForNotificationsToSend.cs deleted file mode 100644 index dc397c48..00000000 --- a/src/Server/OneTrueError.App/Core/Notifications/EventHandlers/CheckForNotificationsToSend.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Incidents.Events; -using OneTrueError.App.Core.Notifications.Tasks; -using OneTrueError.App.Core.Users; - -namespace OneTrueError.App.Core.Notifications.EventHandlers -{ - /// - /// Responsible of sending notifications when a new report have been analyzed. - /// - [Component(RegisterAsSelf = true)] - public class CheckForNotificationsToSend : - IApplicationEventSubscriber - { - private readonly ICommandBus _commandBus; - private readonly INotificationsRepository _notificationsRepository; - private readonly IUserRepository _userRepository; - - /// - /// Creates a new instance of . - /// - /// To load notification configuration - /// To send emails - /// To load user info - public CheckForNotificationsToSend(INotificationsRepository notificationsRepository, ICommandBus commandBus, - IUserRepository userRepository) - { - _notificationsRepository = notificationsRepository; - _commandBus = commandBus; - _userRepository = userRepository; - } - - /// - /// Process an event asynchronously. - /// - /// event to process - /// - /// Task to wait on. - /// - public async Task HandleAsync(ReportAddedToIncident e) - { - if (e == null) throw new ArgumentNullException("e"); - - var settings = await _notificationsRepository.GetAllAsync(e.Incident.ApplicationId); - foreach (var setting in settings) - { - if (setting.NewIncident != NotificationState.Disabled && e.Incident.ReportCount == 1) - { - await CreateNotification(e, setting.AccountId, setting.NewIncident); - } - else if (setting.NewReport != NotificationState.Disabled) - { - await CreateNotification(e, setting.AccountId, setting.NewReport); - } - else if (setting.ReopenedIncident != NotificationState.Disabled && e.IsReOpened) - { - await CreateNotification(e, setting.AccountId, setting.ReopenedIncident); - } - } - } - - private async Task CreateNotification(ReportAddedToIncident e, int accountId, - NotificationState state) - { - if (state == NotificationState.Email) - { - var email = new SendIncidentEmail(_commandBus); - await email.SendAsync(accountId.ToString(), e.Incident, e.Report); - } - else - { - var handler = new SendIncidentSms(_userRepository); - await handler.SendAsync(accountId, e.Incident, e.Report); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Notifications/NotificationState.cs b/src/Server/OneTrueError.App/Core/Notifications/NotificationState.cs deleted file mode 100644 index 3c2e3aa8..00000000 --- a/src/Server/OneTrueError.App/Core/Notifications/NotificationState.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace OneTrueError.App.Core.Notifications -{ - /// - /// Type of notification to use - /// - public enum NotificationState - { - /// - /// Use global setting - /// - UseGlobalSetting, - - /// - /// Do not notify - /// - Disabled, - - /// - /// By cellphone (text message) - /// - Cellphone, - - /// - /// By email - /// - Email - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Notifications/Tasks/SendIncidentEmail.cs b/src/Server/OneTrueError.App/Core/Notifications/Tasks/SendIncidentEmail.cs deleted file mode 100644 index 9e62fb7b..00000000 --- a/src/Server/OneTrueError.App/Core/Notifications/Tasks/SendIncidentEmail.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using OneTrueError.Api.Core.Incidents; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.Api.Core.Reports; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Notifications.Tasks -{ - /// - /// Send incident email - /// - public class SendIncidentEmail - { - private readonly ICommandBus _commandBus; - - /// - /// Creates a new instance of . - /// - /// To send the email - /// commandBus - public SendIncidentEmail(ICommandBus commandBus) - { - if (commandBus == null) throw new ArgumentNullException("commandBus"); - _commandBus = commandBus; - } - - /// - /// Send - /// - /// Account id or email address - /// Incident that the report belongs to - /// Report being processed. - /// task - /// idOrEmailAddress; incident; report - public async Task SendAsync(string idOrEmailAddress, IncidentSummaryDTO incident, ReportDTO report) - { - if (idOrEmailAddress == null) throw new ArgumentNullException("idOrEmailAddress"); - if (incident == null) throw new ArgumentNullException("incident"); - if (report == null) throw new ArgumentNullException("report"); - - var config = ConfigurationStore.Instance.Load(); - - var shortName = incident.Name.Length > 40 - ? incident.Name.Substring(0, 40) + "..." - : incident.Name; - - var baseUrl = string.Format("{0}/#/application/{1}/incident/{2}", - config.BaseUrl.ToString().TrimEnd('/'), - report.ApplicationId, - report.IncidentId); - - //TODO: Add more information - var msg = new EmailMessage(idOrEmailAddress); - if (incident.IsReOpened) - { - msg.Subject = "ReOpened: " + shortName; - msg.TextBody = string.Format(@"Incident: {0} -Report url: {0}/report/{1} -Description: {2} -Exception: {3} - -{4} -", baseUrl, report.Id, incident.Name, report.Exception.FullName, report.Exception.StackTrace); - } - else if (incident.ReportCount == 1) - { - msg.Subject = "New: " + shortName; - msg.TextBody = string.Format(@"Incident: {0} -Description: {1} -Exception: {2} - -{3}", baseUrl, incident.Name, report.Exception.FullName, report.Exception.StackTrace); - } - else - { - msg.Subject = "Updated: " + shortName; - msg.TextBody = string.Format(@"Incident: {0} -Report url: {0}/report/{1} -Description: {2} -Exception: {3} - -{4} -", baseUrl, report.Id, incident.Name, report.Exception.FullName, report.Exception.StackTrace); - } - - var emailCmd = new SendEmail(msg); - await _commandBus.ExecuteAsync(emailCmd); - } - } -} diff --git a/src/Server/OneTrueError.App/Core/Notifications/Tasks/SendIncidentSms.cs b/src/Server/OneTrueError.App/Core/Notifications/Tasks/SendIncidentSms.cs deleted file mode 100644 index 0f351502..00000000 --- a/src/Server/OneTrueError.App/Core/Notifications/Tasks/SendIncidentSms.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using OneTrueError.Api.Core.Incidents; -using OneTrueError.Api.Core.Reports; -using OneTrueError.App.Configuration; -using OneTrueError.App.Core.Users; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Notifications.Tasks -{ - /// - /// Send SMS regarding an incident - /// - public class SendIncidentSms - { - private readonly IUserRepository _userRepository; - - /// - /// Creates a new instance of . - /// - /// to fetch phone number - /// userRepository - public SendIncidentSms(IUserRepository userRepository) - { - if (userRepository == null) throw new ArgumentNullException("userRepository"); - _userRepository = userRepository; - } - - /// - /// Send - /// - /// Account to send to - /// Incident that the report belongs to - /// report being processed - /// task - /// incident;report - /// accountId - public async Task SendAsync(int accountId, IncidentSummaryDTO incident, ReportDTO report) - { - if (incident == null) throw new ArgumentNullException("incident"); - if (report == null) throw new ArgumentNullException("report"); - if (accountId <= 0) throw new ArgumentOutOfRangeException("accountId"); - - var settings = await _userRepository.GetUserAsync(accountId); - if (string.IsNullOrEmpty(settings.MobileNumber)) - return; //TODO: LOG - - var config = ConfigurationStore.Instance.Load(); - var url = config.BaseUrl; - var shortName = incident.Name.Length > 20 - ? incident.Name.Substring(0, 20) + "..." - : incident.Name; - - var exMsg = report.Exception.Message.Length > 100 - ? report.Exception.Message.Substring(0, 100) - : report.Exception.Message; - - string msg; - if (incident.IsReOpened) - { - msg = string.Format(@"ReOpened: {0} -{1}/#/incident/{2} - -{3}", shortName, url, incident.Id, exMsg); - } - else if (incident.ReportCount == 1) - { - msg = string.Format(@"New: {0} -{1}/#/incident/{2} - -Exception: {3}", shortName, url, incident.Id, exMsg); - } - else - { - msg = string.Format(@"Updated: {0} -ReportCount: {4} -{1}/#/incident/{2} - -{3}", shortName, url, incident.Id, exMsg, incident.ReportCount); - } - - var iso = Encoding.GetEncoding("ISO-8859-1"); - var utfBytes = Encoding.UTF8.GetBytes(msg); - var isoBytes = Encoding.Convert(Encoding.UTF8, iso, utfBytes); - msg = iso.GetString(isoBytes); - - var request = - WebRequest.CreateHttp("https://web.smscom.se/sendsms.aspx?acc=ip1-755&pass=z35llww4&msg=" + - Uri.EscapeDataString(msg) + "&to=" + settings.MobileNumber + - "&from=OneTrueError&prio=2"); - request.ContentType = "application/json"; - request.Method = "GET"; - await request.GetResponseAsync(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Reports/Config/ReportConfig.cs b/src/Server/OneTrueError.App/Core/Reports/Config/ReportConfig.cs deleted file mode 100644 index 6ec6ff43..00000000 --- a/src/Server/OneTrueError.App/Core/Reports/Config/ReportConfig.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Reports.Config -{ - /// - /// Configuration settings for reports. - /// - public class ReportConfig : IConfigurationSection - { - /// - /// Max number of reports per incident - /// - /// - /// The oldest report(s) will be deleted if this limit is reached. - /// - public int MaxReportsPerIncident { get; set; } - - /// - /// Number of days to store reports. - /// - /// - /// All reports older than this amount of days will be deleted. - /// - public int RetentionDays { get; set; } - - - string IConfigurationSection.SectionName - { - get { return "ReportConfig"; } - } - - IDictionary IConfigurationSection.ToDictionary() - { - return this.ToConfigDictionary(); - } - - void IConfigurationSection.Load(IDictionary settings) - { - this.AssignProperties(settings); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Reports/IReportsRepository.cs b/src/Server/OneTrueError.App/Core/Reports/IReportsRepository.cs deleted file mode 100644 index 2af8f707..00000000 --- a/src/Server/OneTrueError.App/Core/Reports/IReportsRepository.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Threading.Tasks; -using Griffin.Data; -using OneTrueError.Api.Core.Reports; -using OneTrueError.App.Core.Reports.Invalid; - -namespace OneTrueError.App.Core.Reports -{ - /// - /// Repository for received error reports. - /// - public interface IReportsRepository - { - /// - /// Create a new invalid report - /// - /// report - /// task - /// invalidReport - Task CreateAsync(InvalidErrorReport invalidReport); - - /// - /// Finds the by error identifier asynchronous. - /// - /// Customer generated id (from the client library). - /// report if found; otherwise null. - /// errorId - Task FindByErrorIdAsync(string errorId); - - - /// - /// Get report - /// - /// report id - /// report - /// id - /// no report with that id - Task GetAsync(int id); - - /// - /// Get a list of reports - /// - /// incidentId - /// Page number - /// items per page - /// Paged reports - /// incidentId <= 0 - Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Reports/Invalid/InvalidErrorReport.cs b/src/Server/OneTrueError.App/Core/Reports/Invalid/InvalidErrorReport.cs deleted file mode 100644 index 6635d066..00000000 --- a/src/Server/OneTrueError.App/Core/Reports/Invalid/InvalidErrorReport.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.App.Core.Reports.Invalid -{ - /// - /// Invalid report are reports that we have received from the client library but failed to deserialize or identify. - /// - public class InvalidErrorReport - { - /// - /// Creates a new instance of . - /// - /// primary key - /// applicationId - public InvalidErrorReport(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - AddedAtUtc = DateTime.UtcNow; - } - - /// - /// When report was uploaded - /// - public DateTime AddedAtUtc { get; private set; } - - /// - /// Application id (identified with the help of the AppKey) - /// - public int ApplicationId { get; private set; } - - /// - /// Exception message telling why we couldn't serialize/authenticate it. - /// - public string Exception { get; set; } - - /// - /// Report id - /// - public int Id { get; private set; } - - - /// - /// Report, as uploaded by the client - /// - [SuppressMessage("Microsoft.Performance", "CA1819:PropertiesShouldNotReturnArrays", - Justification = "I like my arrays.")] - public byte[] Report { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Reports/Jobs/DeleteOldReports.cs b/src/Server/OneTrueError.App/Core/Reports/Jobs/DeleteOldReports.cs deleted file mode 100644 index 5629cdf9..00000000 --- a/src/Server/OneTrueError.App/Core/Reports/Jobs/DeleteOldReports.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Threading.Tasks; -using Griffin.ApplicationServices; -using Griffin.Container; -using Griffin.Data; -using log4net; -using OneTrueError.App.Core.Reports.Config; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Reports.Jobs -{ - /// - /// Will delete all reports which is older than the configured () retention - /// period. - /// - [Component(RegisterAsSelf = true)] - public class DeleteOldReports : IBackgroundJobAsync - { - private readonly ILog _logger = LogManager.GetLogger(typeof(DeleteOldReports)); - private readonly IAdoNetUnitOfWork _unitOfWork; - - /// - /// Creates a new instance of . - /// - /// Used for SQL queries - public DeleteOldReports(IAdoNetUnitOfWork unitOfWork) - { - if (unitOfWork == null) throw new ArgumentNullException("unitOfWork"); - _unitOfWork = unitOfWork; - } - - /// - /// Number of reports which can be stored per incident. - /// - public int MaxReportsPerIncident - { - get { return ConfigurationStore.Instance.Load().MaxReportsPerIncident; } - } - - /// - /// Number of days to keep old reports. - /// - public int RetentionDays - { - get - { - var config = ConfigurationStore.Instance.Load(); - return config != null ? config.RetentionDays : 90; - } - } - - /// - public async Task ExecuteAsync() - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - var sql = @"DELETE TOP(1000) FROM ErrorReports WHERE CreatedAtUtc < @date"; - cmd.CommandText = sql; - cmd.AddParameter("date", DateTime.UtcNow.AddDays(-RetentionDays)); - cmd.CommandTimeout = 90; - var rows = await cmd.ExecuteNonQueryAsync(); - if (rows > 0) - { - _logger.Debug("Deleted the oldest " + rows + " reports."); - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs b/src/Server/OneTrueError.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs deleted file mode 100644 index bef4d832..00000000 --- a/src/Server/OneTrueError.App/Core/Reports/Jobs/DeleteReportsBelowReportLimit.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using Griffin.ApplicationServices; -using Griffin.Container; -using Griffin.Data; -using log4net; -using OneTrueError.App.Core.Reports.Config; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Reports.Jobs -{ - /// - /// Delete oldest reports for incidents with report count cap. - /// - /// - /// - /// You can configure the amount of reports per incident in the admin area. - /// - /// - [Component(RegisterAsSelf = true)] - public class DeleteReportsBelowReportLimit : IBackgroundJob - { - private readonly ILog _logger = LogManager.GetLogger(typeof(DeleteReportsBelowReportLimit)); - private readonly IAdoNetUnitOfWork _unitOfWork; - - /// - /// Creates a new instance of . - /// - /// Used for SQL queries - public DeleteReportsBelowReportLimit(IAdoNetUnitOfWork unitOfWork) - { - if (unitOfWork == null) throw new ArgumentNullException("unitOfWork"); - _unitOfWork = unitOfWork; - } - - /// - /// Number of reports which can be stored per incident. - /// - public int MaxReportsPerIncident - { - get - { - var config = ConfigurationStore.Instance.Load(); - if (config == null) - return 5000; - return config.MaxReportsPerIncident; - } - } - - /// - public void Execute() - { - // find incidents with too many reports. - var incidentsToTruncate = new List(); - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = - @"SELECT TOP 10 Id FROM Incidents WHERE ReportCount > @max ORDER BY ReportCount DESC"; - cmd.AddParameter("max", MaxReportsPerIncident); - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - incidentsToTruncate.Add((int) reader[0]); - } - } - } - - foreach (var incidentId in incidentsToTruncate) - { - using (var cmd = _unitOfWork.CreateCommand()) - { - var sql = @"DELETE FROM ErrorReports WHERE IncidentId = @id AND Id <= (SELECT Id -FROM ErrorReports -WHERE IncidentId = @id -ORDER BY Id DESC -OFFSET {0} ROWS -FETCH NEXT 1 ROW ONLY)"; - cmd.CommandText = string.Format(sql, MaxReportsPerIncident); - cmd.AddParameter("id", incidentId); - cmd.CommandTimeout = 90; - var rows = cmd.ExecuteNonQuery(); - if (rows > 0) - { - _logger.Debug("Deleted the oldest " + rows + " reports for incident " + incidentId); - } - } - - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = - @"UPDATE Incidents SET ReportCount = (SELECT count(Id) FROM ErrorReports WHERE IncidentId = @id) WHERE Id = @id"; - cmd.AddParameter("id", incidentId); - cmd.ExecuteNonQuery(); - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Support/SendSupportRequestHandler.cs b/src/Server/OneTrueError.App/Core/Support/SendSupportRequestHandler.cs deleted file mode 100644 index 65d4b6fb..00000000 --- a/src/Server/OneTrueError.App/Core/Support/SendSupportRequestHandler.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Support; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Core.Support -{ - /// - /// Sends a support request to the OneTrueError Team. - /// - /// - /// - /// You must have bought commercial support or registered to get 30 days of free support. - /// - /// - [Component] - public class SendSupportRequestHandler : ICommandHandler - { - /// - public async Task ExecuteAsync(SendSupportRequest command) - { - var baseConfig = ConfigurationStore.Instance.Load(); - var errorConfig = ConfigurationStore.Instance.Load(); - - string installationId = null; - var email = baseConfig.SupportEmail; - if (errorConfig != null) - { - email = errorConfig.ContactEmail; - installationId = errorConfig.InstallationId; - } - - - var items = new List>(); - if (installationId != null) - items.Add(new KeyValuePair("InstallationId", installationId)); - items.Add(new KeyValuePair("ContactEmail", email)); - items.Add(new KeyValuePair("Subject", command.Subject)); - items.Add(new KeyValuePair("Message", command.Message)); - - //To know which page the user had trouble with - items.Add(new KeyValuePair("PageUrl", command.Url)); - - var content = new FormUrlEncodedContent(items); - var client = new HttpClient(); - await client.PostAsync("https://onetrueerror.com/support/request", content); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Users/EventHandlers/CreateOnNewAccount.cs b/src/Server/OneTrueError.App/Core/Users/EventHandlers/CreateOnNewAccount.cs deleted file mode 100644 index 3cb227e0..00000000 --- a/src/Server/OneTrueError.App/Core/Users/EventHandlers/CreateOnNewAccount.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Events; - -namespace OneTrueError.App.Core.Users.EventHandlers -{ - /// - /// Responsible of creating an user entity when a new account is created. - /// - [Component(RegisterAsSelf = true)] - internal class CreateOnNewAccount : IApplicationEventSubscriber - { - private readonly IUserRepository _userRepository; - - public CreateOnNewAccount(IUserRepository userRepository) - { - _userRepository = userRepository; - } - - public async Task HandleAsync(AccountActivated e) - { - await _userRepository.CreateAsync(new User(e.AccountId, e.UserName) - { - EmailAddress = e.EmailAddress - }); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Users/WebApi/GetUserSettingsHandler.cs b/src/Server/OneTrueError.App/Core/Users/WebApi/GetUserSettingsHandler.cs deleted file mode 100644 index 5c227951..00000000 --- a/src/Server/OneTrueError.App/Core/Users/WebApi/GetUserSettingsHandler.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core; -using OneTrueError.Api.Core.Users; -using OneTrueError.Api.Core.Users.Queries; -using OneTrueError.App.Core.Notifications; -using NotificationState = OneTrueError.Api.Core.Users.NotificationState; - -namespace OneTrueError.App.Core.Users.WebApi -{ - [Component] - internal class GetUserSettingsHandler : IQueryHandler - { - private readonly INotificationsRepository _repository; - private readonly IUserRepository _userRepository; - - public GetUserSettingsHandler(INotificationsRepository repository, IUserRepository userRepository) - { - _repository = repository; - _userRepository = userRepository; - } - - public async Task ExecuteAsync(GetUserSettings query) - { - var settings = await _repository.TryGetAsync(query.UserId, query.ApplicationId) - ?? new UserNotificationSettings(query.UserId, query.ApplicationId); - var user = await _userRepository.GetUserAsync(query.UserId); - return new GetUserSettingsResult - { - FirstName = user.FirstName, - LastName = user.LastName, - MobileNumber = user.MobileNumber, - Notifications = new NotificationSettings - { - NotifyOnReOpenedIncident = settings.ReopenedIncident.ConvertEnum(), - NotifyOnUserFeedback = settings.UserFeedback.ConvertEnum(), - NotifyOnPeaks = settings.ApplicationSpike.ConvertEnum(), - NotifyOnNewReport = settings.NewReport.ConvertEnum(), - NotifyOnNewIncidents = settings.NewIncident.ConvertEnum() - } - }; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Core/Users/WebApi/UpdatePersonalSettingsHandler.cs b/src/Server/OneTrueError.App/Core/Users/WebApi/UpdatePersonalSettingsHandler.cs deleted file mode 100644 index cf693c0f..00000000 --- a/src/Server/OneTrueError.App/Core/Users/WebApi/UpdatePersonalSettingsHandler.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Users.Commands; - -namespace OneTrueError.App.Core.Users.WebApi -{ - /// - /// Handler for . - /// - [Component] - public class UpdatePersonalSettingsHandler : ICommandHandler - { - private readonly IUserRepository _userRepository; - - /// - /// Creates a new instance of . - /// - /// repos - /// userRepository - public UpdatePersonalSettingsHandler(IUserRepository userRepository) - { - if (userRepository == null) throw new ArgumentNullException("userRepository"); - _userRepository = userRepository; - } - - /// - /// Execute a command asynchronously. - /// - /// Command to execute. - /// - /// Task which will be completed once the command has been executed. - /// - public async Task ExecuteAsync(UpdatePersonalSettings command) - { - if (command == null) throw new ArgumentNullException("command"); - - var user = await _userRepository.GetUserAsync(command.UserId); - user.FirstName = command.FirstName; - user.LastName = command.LastName; - user.MobileNumber = command.MobileNumber; - await _userRepository.UpdateAsync(user); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/GlobalSuppressions.cs b/src/Server/OneTrueError.App/GlobalSuppressions.cs deleted file mode 100644 index 530c4cff..00000000 --- a/src/Server/OneTrueError.App/GlobalSuppressions.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", Target = "*")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Modules.Tagging.Handlers.GetTagsForIncidentHandler.#.ctor(OneTrueError.App.Modules.Tagging.ITagsRepository)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Applications.QueryHandlers.GetApplicationTeamHandler.#.ctor(OneTrueError.App.Core.Applications.IApplicationRepository)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Accounts.Account.#Id")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Feedback.EventSubscribers.AttachFeedbackToIncident.#.ctor(OneTrueError.App.Core.Feedback.IFeedbackRepository)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Applications.CommandHandlers.CreateApplicationHandler.#.ctor(OneTrueError.App.Core.Applications.IApplicationRepository,OneTrueError.App.Core.Users.IUserRepository,DotNetCqs.IEventBus)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Applications.EventHandlers.CreateDefaultAppOnAccountActivated.#.ctor(DotNetCqs.ICommandBus)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Users.EventHandlers.CreateOnNewAccount.#.ctor(OneTrueError.App.Core.Users.IUserRepository)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Modules.Messaging.Templating.DateFormatter.#FormatAgo(System.DateTime)")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Users.WebApi.GetUserSettingsHandler.#.ctor(OneTrueError.App.Core.Notifications.INotificationsRepository,OneTrueError.App.Core.Users.IUserRepository)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Incidents.Incident.#ApplicationId")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Incidents.Incident.#CreatedAtUtc")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Feedback.InvalidErrorReport.#Id")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Reports.Invalid.InvalidErrorReport.#Id")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Invitations.CommandHandlers.InviteUserHandler.#.ctor(OneTrueError.App.Core.Invitations.Data.IInvitationRepository,DotNetCqs.IEventBus,OneTrueError.App.Core.Users.IUserRepository,OneTrueError.App.Core.Applications.IApplicationRepository,DotNetCqs.ICommandBus)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Modules.Triggers.Domain.Actions.SendEmailTask.#.ctor(DotNetCqs.ICommandBus)")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Applications.EventHandlers.UpdateTeamOnInvitationAccepted.#.ctor(OneTrueError.App.Core.Applications.IApplicationRepository)" - )] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Configuration.ErrorTracking")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Configuration.Messaging")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Accounts")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Accounts.CommandHandlers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Accounts.Queries")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Applications")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Applications.QueryHandlers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Feedback")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Feedback.EventSubscribers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Incidents")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Incidents.Commands")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Invitations")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Invitations.Data")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Invitations.EventHandlers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Notifications")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Notifications.EventHandlers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Notifications.Tasks")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Reports")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Reports.Invalid")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Reports.Queries")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Users")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Core.Users.WebApi")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Geolocation")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Geolocation.EventHandlers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Messaging")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Messaging.Commands")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.ReportSpikes")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Similarities.Domain.Adapters.Normalizers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Similarities.Domain.Adapters.OperatingSystems")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Similarities.EventHandlers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Tagging.Domain")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Tagging.Handlers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Triggers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Triggers.Commands")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Triggers.Domain.Actions.Tools")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Triggers.Domain.Rules")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Triggers.EventHandlers")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Triggers.Queries")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re", Scope = "member", - Target = "OneTrueError.App.Modules.Triggers.Domain.Trigger.#RunForReOpenedIncidents")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Re", Scope = "member", - Target = "OneTrueError.App.Core.Notifications.UserNotificationSettings.#ReOpenedIncident")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ROLE", Scope = "member", - Target = "OneTrueError.App.Core.Applications.Application.#ROLE_ADMIN")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "ROLE", Scope = "member", - Target = "OneTrueError.App.Core.Applications.Application.#ROLE_MEMBER")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "SEQUENCE", - Scope = "member", Target = "OneTrueError.App.Core.Accounts.Account.#SEQUENCE")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Api", - Scope = "namespace", Target = "OneTrueError.App.Core.Users.WebApi")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "Username", - Scope = "member", - Target = "OneTrueError.App.Core.Accounts.IAccountRepository.#FindByUsernameAsync(System.String)")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterCondition", - Scope = "type", Target = "OneTrueError.App.Modules.Triggers.Domain.FilterCondition")] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterCondition", - Scope = "member", - Target = - "OneTrueError.App.Modules.Triggers.Commands.DtoToDomainConverters.#ConvertFilterCondition(OneTrueError.Api.Modules.Triggers.TriggerFilterCondition)" - )] -[assembly: - SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterCondition", - Scope = "member", - Target = - "OneTrueError.App.Modules.Triggers.Queries.DomainToDtoConverters.#ConvertFilterCondition(OneTrueError.App.Modules.Triggers.Domain.FilterCondition)" - )] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = - "OneTrueError.App.Core.Invitations.CommandHandlers.AcceptInvitationHandler.#.ctor(OneTrueError.App.Core.Invitations.Data.IInvitationRepository,OneTrueError.App.Core.Accounts.IAccountRepository,DotNetCqs.IEventBus)" - )] -[assembly: - SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "1", Scope = "member", - Target = - "OneTrueError.App.Modules.Similarities.Domain.Adapters.OperatingSystemAdapter.#Adapt(OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner.ValueAdapterContext,System.Object)" - )] -[assembly: SuppressMessage("Microsoft.Design", "CA1014:MarkAssembliesWithClsCompliant")] -[assembly: - SuppressMessage("Microsoft.Design", "CA1020:AvoidNamespacesWithFewTypes", Scope = "namespace", - Target = "OneTrueError.App.Modules.Tagging")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Feedback.FeedbackEntity.#CreatedAtUtc")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Feedback.FeedbackEntity.#Description")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Feedback.FeedbackEntity.#EmailAddress")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Feedback.FeedbackEntity.#ErrorId")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Incidents.Incident.#Id")] -[assembly: - SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Scope = "member", - Target = "OneTrueError.App.Core.Invitations.Invitation.#Id")] \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Geolocation/ErrorOrginResult.cs b/src/Server/OneTrueError.App/Modules/Geolocation/ErrorOrginResult.cs deleted file mode 100644 index c86dbf9e..00000000 --- a/src/Server/OneTrueError.App/Modules/Geolocation/ErrorOrginResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OneTrueError.App.Modules.Geolocation -{ - /// - /// List result item for repository queries - /// - public class ErrorOrginListItem - { - /// - /// Latitude - /// - public double Latitude { get; set; } - - /// - /// Longitude - /// - public double Longitude { get; set; } - - /// - /// Number of error reports that have been received from this incident - /// - public int NumberOfErrorReports { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Geolocation/ErrorOrigin.cs b/src/Server/OneTrueError.App/Modules/Geolocation/ErrorOrigin.cs deleted file mode 100644 index 6ea7eae1..00000000 --- a/src/Server/OneTrueError.App/Modules/Geolocation/ErrorOrigin.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.App.Modules.Geolocation -{ - /// - /// Geographic location of where an error report originated from. - /// - public class ErrorOrigin - { - /// - /// Creates a new instance of . - /// - /// IP address that we received the report from - /// Longitude that the IP lookup service returned. - /// Latitude that the IP lookup service returned. - public ErrorOrigin(string ipAddress, double longitude, double latitude) - { - if (ipAddress == null) throw new ArgumentNullException("ipAddress"); - - IpAddress = ipAddress; - Longitude = longitude; - Latitude = latitude; - } - - - /// - /// City reported by the lookup service. - /// - public string City { get; set; } - - /// - /// Country code (top domain) - /// - public string CountryCode { get; set; } - - - /// - /// Name of countrt - /// - public string CountryName { get; set; } - - /// - /// IP address that we received the report from - /// - [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Ip")] - public string IpAddress { get; private set; } - - /// Longitude that the IP lookup service returned. - public double Latitude { get; private set; } - - /// Latitude that the IP lookup service returned. - public double Longitude { get; private set; } - - /// - /// TODO: WTF IS THIS?! - /// - public string RegionCode { get; set; } - - /// - /// Country name - /// - public string RegionName { get; set; } - - /// - /// Zip / postal code. - /// - public string ZipCode { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Geolocation/EventHandlers/StorePositionFromNewReport.cs b/src/Server/OneTrueError.App/Modules/Geolocation/EventHandlers/StorePositionFromNewReport.cs deleted file mode 100644 index 68b1363c..00000000 --- a/src/Server/OneTrueError.App/Modules/Geolocation/EventHandlers/StorePositionFromNewReport.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Globalization; -using System.IO; -using System.Net; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using Newtonsoft.Json.Linq; -using OneTrueError.Api.Core.Incidents.Events; - -namespace OneTrueError.App.Modules.Geolocation.EventHandlers -{ - /// - /// Responsible of looking up geographic position of the IP address that delivered the report. - /// - [Component(RegisterAsSelf = true)] - public class StorePositionFromNewReport : IApplicationEventSubscriber - { - private readonly ILog _logger = LogManager.GetLogger(typeof(StorePositionFromNewReport)); - private readonly IErrorOriginRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// repository - public StorePositionFromNewReport(IErrorOriginRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - /// Process an event asynchronously. - /// - /// event to process - /// - /// Task to wait on. - /// - public async Task HandleAsync(ReportAddedToIncident e) - { - if (string.IsNullOrEmpty(e.Report.RemoteAddress)) - return; - - if (e.Report.RemoteAddress == "::1") - return; - if (e.Report.RemoteAddress == "127.0.0.1") - return; - - var request = WebRequest.CreateHttp("http://freegeoip.net/json/" + e.Report.RemoteAddress); - string json = ""; - try - { - var response = await request.GetResponseAsync(); - var stream = response.GetResponseStream(); - var reader = new StreamReader(stream); - json = await reader.ReadToEndAsync(); - var jsonObj = JObject.Parse(json); - - /* /*{"ip":"94.254.21.175","country_code":"SE","country_name":"Sweden","region_code":"10","region_name":"Dalarnas Lan","city":"Falun","zipcode":"", - * "latitude":60.6,"longitude":15.6333, - * "metro_code":"","areacode":""}*/ - - var lat = double.Parse(jsonObj["latitude"].Value(), CultureInfo.InvariantCulture); - var lon = double.Parse(jsonObj["longitude"].Value(), CultureInfo.InvariantCulture); - var cmd = new ErrorOrigin(e.Report.RemoteAddress, lon, lat) - { - City = jsonObj["city"].ToString(), - CountryCode = jsonObj["country_code"].ToString(), - CountryName = jsonObj["country_name"].ToString(), - RegionCode = jsonObj["region_code"].ToString(), - RegionName = jsonObj["region_name"].ToString(), - ZipCode = jsonObj["zip_code"].ToString() - }; - - await _repository.CreateAsync(cmd, e.Incident.ApplicationId, e.Incident.Id, e.Report.Id); - } - catch (Exception exception) - { - _logger.Error("Failed to store location: " + json, exception); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Geolocation/IErrorOriginRepository.cs b/src/Server/OneTrueError.App/Modules/Geolocation/IErrorOriginRepository.cs deleted file mode 100644 index 4eadb623..00000000 --- a/src/Server/OneTrueError.App/Modules/Geolocation/IErrorOriginRepository.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace OneTrueError.App.Modules.Geolocation -{ - /// - /// Stores error origins - /// - /// - /// TODO: Uses the IncidentTabl - /// - public interface IErrorOriginRepository - { - /// - /// Create a new entry - /// - /// origin - /// Application that we received a report for - /// incident that the report belongs to - /// report received that we got a location for - /// task - /// origin - Task CreateAsync(ErrorOrigin origin, int applicationId, int incidentId, int reportId); - - /// - /// Find all origins for a specific incident - /// - /// incident - /// All locations. - Task> FindForIncidentAsync(int incidentId); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Geolocation/QueryHandlers/GetOriginsForIncidentHandler.cs b/src/Server/OneTrueError.App/Modules/Geolocation/QueryHandlers/GetOriginsForIncidentHandler.cs deleted file mode 100644 index 0f5d6e44..00000000 --- a/src/Server/OneTrueError.App/Modules/Geolocation/QueryHandlers/GetOriginsForIncidentHandler.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Modules.ErrorOrigins.Queries; - -namespace OneTrueError.App.Modules.Geolocation.QueryHandlers -{ - /// - /// Handler for . - /// - [Component] - public class GetOriginsForIncidentHandler : IQueryHandler - { - private readonly IErrorOriginRepository _repository; - - - /// - /// Creates a new instance of . - /// - /// repos - /// repository - public GetOriginsForIncidentHandler(IErrorOriginRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - /// Method used to execute the query - /// - /// Query to execute. - /// - /// Task which will contain the result once completed. - /// - public async Task ExecuteAsync(GetOriginsForIncident query) - { - var reports = await _repository.FindForIncidentAsync(query.IncidentId); - var items = from x in reports - select new GetOriginsForIncidentResultItem - { - Longitude = x.Longitude, - Latitude = x.Latitude, - NumberOfErrorReports = x.NumberOfErrorReports - }; - return new GetOriginsForIncidentResult {Items = items.ToArray()}; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Commands/SendEmailHandler.cs b/src/Server/OneTrueError.App/Modules/Messaging/Commands/SendEmailHandler.cs deleted file mode 100644 index 3bce20c7..00000000 --- a/src/Server/OneTrueError.App/Modules/Messaging/Commands/SendEmailHandler.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System; -using System.IO; -using System.Net; -using System.Net.Mail; -using System.Net.Mime; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Queries; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Infrastructure.Net; - -namespace OneTrueError.App.Modules.Messaging.Commands -{ - [Component] - internal class SendEmailHandler : ICommandHandler - { - private readonly IQueryBus _queryBus; - - public SendEmailHandler(IQueryBus queryBus) - { - if (queryBus == null) throw new ArgumentNullException("queryBus"); - _queryBus = queryBus; - } - -#pragma warning disable 1998 - public async Task ExecuteAsync(SendEmail command) -#pragma warning restore 1998 - { - var client = CreateSmtpClient(); - if (client == null) - return; - - var baseConfig = ConfigurationStore.Instance.Load(); - - var email = new MailMessage - { - From = new MailAddress(baseConfig.SupportEmail), - Subject = command.EmailMessage.Subject - }; - foreach (var recipient in command.EmailMessage.Recipients) - { - int accountId; - if (int.TryParse(recipient.Address, out accountId)) - { - var query = new GetAccountEmailById(accountId); - var emailAddress = await _queryBus.QueryAsync(query); - email.To.Add(new MailAddress(emailAddress, recipient.Name)); - } - else - email.To.Add(new MailAddress(recipient.Address, recipient.Name)); - } - if (string.IsNullOrEmpty(command.EmailMessage.HtmlBody)) - { - email.Body = command.EmailMessage.TextBody; - email.IsBodyHtml = false; - await client.SendMailAsync(email); - return; - } - - var av = AlternateView.CreateAlternateViewFromString(command.EmailMessage.HtmlBody, null, - MediaTypeNames.Text.Html); - if (!string.IsNullOrEmpty(command.EmailMessage.TextBody)) - email.Body = command.EmailMessage.TextBody; - foreach (var resource in command.EmailMessage.Resources) - { - var contentType = new ContentType(MimeMapping.GetMimeType(Path.GetExtension(resource.Name))); - var linkedResource = new LinkedResource(resource.Name, contentType); - await resource.Content.CopyToAsync(linkedResource.ContentStream); - av.LinkedResources.Add(linkedResource); - } - email.AlternateViews.Add(av); - await client.SendMailAsync(email); - } - - private static SmtpClient CreateSmtpClient() - { - var config = ConfigurationStore.Instance.Load(); - if (string.IsNullOrEmpty(config.SmtpHost)) - return null; - - var client = new SmtpClient(config.SmtpHost, config.PortNumber); - if (!string.IsNullOrEmpty(config.AccountName)) - client.Credentials = new NetworkCredential(config.AccountName, config.AccountPassword); - client.EnableSsl = config.UseSsl; - return client; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Commands/SendTemplateEmailHandler.cs b/src/Server/OneTrueError.App/Modules/Messaging/Commands/SendTemplateEmailHandler.cs deleted file mode 100644 index 06fe59a8..00000000 --- a/src/Server/OneTrueError.App/Modules/Messaging/Commands/SendTemplateEmailHandler.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.IO; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Modules.Messaging.Templating; - -namespace OneTrueError.App.Modules.Messaging.Commands -{ - /// - /// Send an email using a template. - /// - [Component] - public class SendTemplateEmailHandler : ICommandHandler - { - private readonly ICommandBus _commandBus; - - /// - /// Creates a new instance of . - /// - /// Used to send the command. - /// commandBus - public SendTemplateEmailHandler(ICommandBus commandBus) - { - if (commandBus == null) throw new ArgumentNullException("commandBus"); - _commandBus = commandBus; - } - - /// - /// Execute a command asynchronously. - /// - /// Command to execute. - /// - /// Task which will be completed once the command has been executed. - /// - public async Task ExecuteAsync(SendTemplateEmail command) - { - var loader = new TemplateLoader(); - var templateParser = new TemplateParser(); - - var layout = loader.Load("Layout"); - - var template = loader.Load(command.TemplateName); - var html = templateParser.RunAll(template, command.Model); - if (html.IndexOf("src=\"cid:", StringComparison.OrdinalIgnoreCase) == -1) - html = html.Replace(@"src=""", @"src=""cid:"); - - string complete; - try - { - complete = templateParser.RunFormatterOnly(layout, - new {Title = command.MailTitle, Body = html}); - } - catch (Exception) - { - throw; - } - - var msg = new EmailMessage(command.To) {Subject = command.Subject, HtmlBody = complete}; - - foreach (var resource in template.Resources) - { - var linkedResource = new EmailResource(resource.Key, resource.Value); - - var reader = new BinaryReader(resource.Value); - var dimensions = ImageHelper.GetDimensions(reader); - var key = string.Format("src=\"cid:{0}\"", resource.Key); - complete = complete.Replace(key, - string.Format("{0} width=\"{1}\" height=\"{2}\" style=\"border: 1px solid #000\"", key, - dimensions.Width, dimensions.Height)); - resource.Value.Position = 0; - - msg.Resources.Add(linkedResource); - } - foreach (var resource in layout.Resources) - { - var linkedResource = new EmailResource(resource.Key, resource.Value); - - var reader = new BinaryReader(resource.Value); - var dimensions = ImageHelper.GetDimensions(reader); - var key = string.Format("src=\"cid:{0}\"", resource.Key); - complete = complete.Replace(key, - string.Format("{0} width=\"{1}\" height=\"{2}\"", key, dimensions.Width, dimensions.Height)); - resource.Value.Position = 0; - - msg.Resources.Add(linkedResource); - } - if (command.Resources != null) - { - foreach (var resource in command.Resources) - { - msg.Resources.Add(resource); - } - } - msg.HtmlBody = complete; - - var sendEmail = new SendEmail(msg); - await _commandBus.ExecuteAsync(sendEmail); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Layout/logo2.png b/src/Server/OneTrueError.App/Modules/Messaging/Templating/Layout/logo2.png deleted file mode 100644 index 474efb38..00000000 Binary files a/src/Server/OneTrueError.App/Modules/Messaging/Templating/Layout/logo2.png and /dev/null differ diff --git a/src/Server/OneTrueError.App/Modules/ReportSpikes/CheckForReportPeak.cs b/src/Server/OneTrueError.App/Modules/ReportSpikes/CheckForReportPeak.cs deleted file mode 100644 index b353430d..00000000 --- a/src/Server/OneTrueError.App/Modules/ReportSpikes/CheckForReportPeak.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Incidents.Events; -using OneTrueError.Api.Core.Messaging; -using OneTrueError.Api.Core.Messaging.Commands; -using OneTrueError.App.Configuration; -using OneTrueError.App.Core.Notifications; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.App.Modules.ReportSpikes -{ - /// - /// - [Component(RegisterAsSelf = true)] - public class CheckForReportPeak : - IApplicationEventSubscriber - { - private readonly ICommandBus _commandBus; - private readonly INotificationsRepository _repository; - private readonly IReportSpikeRepository _spikeRepository; - - /// - /// Creates a new instance of . - /// - /// To check if spikes should be analysed - /// store/fetch information of current spikes. - /// used to send emails - public CheckForReportPeak(INotificationsRepository repository, IReportSpikeRepository spikeRepository, - ICommandBus commandBus) - { - _repository = repository; - _spikeRepository = spikeRepository; - _commandBus = commandBus; - } - - /// - /// Process an event asynchronously. - /// - /// event to process - /// - /// Task to wait on. - /// - public async Task HandleAsync(ReportAddedToIncident e) - { - if (e == null) throw new ArgumentNullException("e"); - - var config = ConfigurationStore.Instance.Load(); - var url = config.BaseUrl; - var settings = await _repository.GetAllAsync(e.Report.ApplicationId); - if (!settings.Any(x => x.ApplicationSpike != NotificationState.Disabled)) - return; - - var todaysCount = await CalculateSpike(e); - if (todaysCount == null) - return; - - var spike = await _spikeRepository.GetSpikeAsync(e.Incident.ApplicationId); - if (spike != null) - spike.IncreaseReportCount(); - - var existed = spike != null; - var messages = new List(); - foreach (var setting in settings) - { - if (setting.ApplicationSpike != NotificationState.Disabled) - continue; - - if (spike != null && spike.HasAccount(setting.AccountId)) - continue; - - if (spike == null) - spike = new ErrorReportSpike(e.Incident.ApplicationId, 1); - - spike.AddNotifiedAccount(setting.AccountId); - var msg = new EmailMessage(setting.AccountId.ToString()) - { - Subject = string.Format("Spike detected for {0} ({1} reports)", - e.Incident.ApplicationName, - todaysCount), - TextBody = - string.Format( - "We've detected a spike in incoming reports for application {2}\r\n" + - "\r\n" + - "We've received {3} reports so far. Day average is {4}\r\n" + - "\r\n" + - "No further spike emails will be sent today for that application.", - url, - e.Incident.ApplicationId, - e.Incident.ApplicationName, todaysCount.SpikeCount, todaysCount.DayAverage) - }; - - messages.Add(msg); - } - - if (existed) - await _spikeRepository.UpdateSpikeAsync(spike); - else - await _spikeRepository.CreateSpikeAsync(spike); - - foreach (var message in messages) - { - var sendEmail = new SendEmail(message); - await _commandBus.ExecuteAsync(sendEmail); - } - } - - /// - /// Compare received amount of report with a calculated threshold. - /// - /// e - /// -1 if no spike is detected; otherwise the spike count - protected async Task CalculateSpike(ReportAddedToIncident applicationEvent) - { - if (applicationEvent == null) throw new ArgumentNullException("applicationEvent"); - - var average = await _spikeRepository.GetAverageReportCountAsync(applicationEvent.Incident.ApplicationId); - if (average == 0) - return null; - - var todaysCount = await _spikeRepository.GetTodaysCountAsync(applicationEvent.Incident.ApplicationId); - var threshold = average > 20 ? average : average*2; - if (todaysCount < threshold) - return null; - - return new NewSpike - { - SpikeCount = todaysCount, - DayAverage = average - }; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/ReportSpikes/IReportSpikeRepository.cs b/src/Server/OneTrueError.App/Modules/ReportSpikes/IReportSpikeRepository.cs deleted file mode 100644 index 99518315..00000000 --- a/src/Server/OneTrueError.App/Modules/ReportSpikes/IReportSpikeRepository.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Threading.Tasks; -using Griffin.Data; - -namespace OneTrueError.App.Modules.ReportSpikes -{ - /// - /// Repostory for spikes. - /// - public interface IReportSpikeRepository - { - /// - /// Create spike - /// - /// spike - /// task - /// applicationId - Task CreateSpikeAsync(ErrorReportSpike spike); - - /// - /// Get average day count for application - /// - /// application id - /// count - /// applicationId - Task GetAverageReportCountAsync(int applicationId); - - /// - /// Get a spike - /// - /// - /// - /// Failed to find a spike for the specified application - /// applicationId - Task GetSpikeAsync(int applicationId); - - /// - /// Get number of reports for today - /// - /// application id - /// count - /// applicationId - Task GetTodaysCountAsync(int applicationId); - - /// - /// Update spike - /// - /// spike - /// task - /// spike - Task UpdateSpikeAsync(ErrorReportSpike spike); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/ReportSpikes/NewSpike.cs b/src/Server/OneTrueError.App/Modules/ReportSpikes/NewSpike.cs deleted file mode 100644 index f01cd6e7..00000000 --- a/src/Server/OneTrueError.App/Modules/ReportSpikes/NewSpike.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace OneTrueError.App.Modules.ReportSpikes -{ - /// - /// Detected a new spike - /// - public class NewSpike - { - /// - /// Typical report count average per day - /// - public int DayAverage { get; set; } - - /// - /// Current report count - /// - public int SpikeCount { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/NamespaceDoc.cs b/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/NamespaceDoc.cs deleted file mode 100644 index 6469636f..00000000 --- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/Adapters/NamespaceDoc.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.App.Modules.Similarities.Domain.Adapters -{ - /// - /// För att kunna identifiera likheter behöver absoluta värden att normaliseras. Exempelvis säger inte "483 handles" - /// någonting, men om man grupperar i > 10, > 100, > 1000, > 10000 så säger det mer. - /// Notera att grundvärdena kan man fortfarande få genom att titta på varje incident. - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/NamespaceDoc.cs b/src/Server/OneTrueError.App/Modules/Similarities/Domain/NamespaceDoc.cs deleted file mode 100644 index e69476f4..00000000 --- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/NamespaceDoc.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.App.Modules.Similarities.Domain -{ - /// - /// Similarities are used to analyze every error report to detect what all reports for an incident have in common. - /// - /// - /// This is great if you get an exception on just some of the client computers. - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilaritiesReport.cs b/src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilaritiesReport.cs deleted file mode 100644 index 087ae514..00000000 --- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/SimilaritiesReport.cs +++ /dev/null @@ -1,166 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using OneTrueError.Api.Core.Reports; -using OneTrueError.App.Modules.Similarities.Domain.Adapters; -using OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner; - -namespace OneTrueError.App.Modules.Similarities.Domain -{ - /// - /// Stores information about all context collection properties and how often their different values are the same. - /// - /// - /// For instance. 90 reports may have v4.0.0 of the assembly Framework.Core while 10 reports have v3.5.0. that means - /// that v4.0.0 is used in 90% of the reports and therefore the - /// assembly that the support team should use when trying to find the exception. - /// - public class SimilaritiesReport - { - private static readonly string[] IgnoredCollections = {"OpenForms", "Screenshots"}; - private readonly List _collections = new List(); - - - /// - /// Creates a new instance of . - /// - /// Incident that this report belongs to - /// incidentId - public SimilaritiesReport(int incidentId) - { - if (incidentId < 1) throw new ArgumentNullException("incidentId"); - IncidentId = incidentId; - ReportCount = 0; - } - - /// - /// Serialization constructor - /// - protected SimilaritiesReport() - { - } - - /// - /// Creates a new instance of . - /// - /// Incident that this report belongs to - /// all generated collections - /// collections - /// incidentId - [SuppressMessage("Microsoft.Design", "CA1002:DoNotExposeGenericLists")] - public SimilaritiesReport(int incidentId, List collections) - { - if (collections == null) throw new ArgumentNullException("collections"); - if (incidentId <= 0) throw new ArgumentOutOfRangeException("incidentId"); - IncidentId = incidentId; - _collections = collections; - } - - /// - /// Collections - /// - public IEnumerable Collections - { - get { return _collections; } - } - - /// - /// Incident that this is a analysis for. - /// - public int IncidentId { get; private set; } - - /// - /// Number of reports for the incident - /// - public int ReportCount { get; private set; } - - /// - /// Add a new report. - /// - /// report - /// adapters - public void AddReport(ReportDTO report, IReadOnlyList adapters) - { - if (report == null) throw new ArgumentNullException("report"); - if (adapters == null) throw new ArgumentNullException("adapters"); - - //TODO: Varför skicka in adapters? Skapa via en Factory istället - if (report == null) throw new ArgumentNullException("report"); - - ReportCount += 1; - - foreach (var context in report.ContextCollections) - { - if (context.Name == null) - throw new ArgumentException("ContextInfo.Name may not be null."); - - if (IgnoredCollections.Contains(context.Name, StringComparer.OrdinalIgnoreCase)) - continue; - - foreach (var property in context.Properties) - { - if (property.Value != null && property.Value.Length > 40) - continue; - - if (property.Key.Equals("OEMStringArray")) - continue; - if (context.Name.Equals("ExceptionProperties") && property.Key == "Message") - continue; - if (context.Name.Equals("ExceptionProperties") && property.Key == "StackTrace") - continue; - if (context.Name.Equals("ExceptionProperties") && property.Key == "InnerException") - continue; - if (property.Key.Contains("LastModified")) - continue; - if (property.Key == "Id") - continue; - - var adapterContext = new ValueAdapterContext(context.Name, property.Key, property.Value, report); - object adaptedValue = property.Value; - foreach (var adapter in adapters) - { - adaptedValue = adapter.Adapt(adapterContext, adaptedValue); - } - - foreach (var field in adapterContext.CustomFields) - { - AddSimilarity(field.ContextName, field.PropertyName, field.Value); - } - - if (adapterContext.IgnoreProperty || "".Equals(adaptedValue)) - continue; - - if (adaptedValue == null) - adaptedValue = adapterContext.Value ?? "null"; - AddSimilarity(context.Name, adapterContext.PropertyName, adaptedValue); - //similarity.IncreaseUsage(ReportCount); - } - } - } - - /// - /// Get a specific collection - /// - /// context collection name - /// collection if found; otherwise null. - protected SimilarityCollection GetCollection(string contextName) - { - if (contextName == null) throw new ArgumentNullException("contextName"); - - return _collections.FirstOrDefault(x => x.Name.Equals(contextName, StringComparison.OrdinalIgnoreCase)); - } - - private void AddSimilarity(string contextName, string propertyName, object adaptedValue) - { - var collection = GetCollection(contextName); - if (collection == null) - { - collection = new SimilarityCollection(IncidentId, contextName); - _collections.Add(collection); - } - - collection.Add(propertyName, adaptedValue); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Similarities/Domain/WmiDateConverter.cs b/src/Server/OneTrueError.App/Modules/Similarities/Domain/WmiDateConverter.cs deleted file mode 100644 index 22353320..00000000 --- a/src/Server/OneTrueError.App/Modules/Similarities/Domain/WmiDateConverter.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Management; -using log4net; - -namespace OneTrueError.App.Modules.Similarities.Domain -{ - /// - /// Translates WMI dates to .NETs DateTime. - /// - public static class WmiDateConverter - { - private static readonly ILog _logger = LogManager.GetLogger(typeof(WmiDateConverter)); - - /// - /// Try parse a WMI date - /// - /// date - /// converted date - /// true if successful; otherwise false. - /// date - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] - public static bool TryParse(string date, out DateTime result) - { - if (date == null) throw new ArgumentNullException("date"); - - if (date.Length < 22 || date.Length > 26 || date[14] != '.' || date[0] != '2' || date[1] != '0') - return DateTime.TryParse(date, out result); - - try - { - result = ManagementDateTimeConverter.ToDateTime(date); - return true; - } - catch (Exception ex) - { - _logger.Error("Failed to convert " + date + ".", ex); - } - - return DateTime.TryParse(date, out result); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Similarities/EventHandlers/UpdateSimilaritiesFromNewReport.cs b/src/Server/OneTrueError.App/Modules/Similarities/EventHandlers/UpdateSimilaritiesFromNewReport.cs deleted file mode 100644 index c418963d..00000000 --- a/src/Server/OneTrueError.App/Modules/Similarities/EventHandlers/UpdateSimilaritiesFromNewReport.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Incidents.Events; -using OneTrueError.App.Modules.Similarities.Domain; -using OneTrueError.App.Modules.Similarities.Domain.Adapters.Runner; - -namespace OneTrueError.App.Modules.Similarities.EventHandlers -{ - /// - /// Responsible of analyzing the reports Context Data to find similarities from all reports in an incident. - /// - [Component(RegisterAsSelf = true)] - public class UpdateSimilaritiesFromNewReport : IApplicationEventSubscriber - { - private readonly AdapterRepository _adapterRepository = new AdapterRepository(); - private readonly ILog _logger = LogManager.GetLogger(typeof(UpdateSimilaritiesFromNewReport)); - private readonly ISimilarityRepository _similarityRepository; - - /// - /// Creates a new instance of . - /// - /// epos - /// similarityReposiotry - public UpdateSimilaritiesFromNewReport(ISimilarityRepository similarityRepository) - { - if (similarityRepository == null) throw new ArgumentNullException("similarityRepository"); - _similarityRepository = similarityRepository; - } - - /// - /// Process an event asynchronously. - /// - /// event to process - /// - /// Task to wait on. - /// - public async Task HandleAsync(ReportAddedToIncident e) - { - _logger.Debug("Updating similarities"); - var adapters = _adapterRepository.GetAdapters(); - var sw2 = new Stopwatch(); - sw2.Start(); - long step1, step2, step3; - - try - { - _logger.Debug("Finding for incident: " + e.Incident.Id); - var similarity = _similarityRepository.FindForIncident(e.Incident.Id); - - step1 = sw2.ElapsedMilliseconds; - var isNew = false; - if (similarity == null) - { - _logger.Debug("Not found, creating a new"); - similarity = new SimilaritiesReport(e.Incident.Id); - isNew = true; - } - - step2 = sw2.ElapsedMilliseconds; - similarity.AddReport(e.Report, adapters); - - if (isNew) - { - _logger.Debug("Creating..."); - await _similarityRepository.CreateAsync(similarity); - } - else - { - _logger.Debug("Updating..."); - await _similarityRepository.UpdateAsync(similarity); - } - - step3 = sw2.ElapsedMilliseconds; - _logger.Debug("similarities done "); - } - catch (Exception exception) - { - _logger.Error("failed to add report to incident " + e.Incident.Id, exception); - throw; - } - sw2.Stop(); - if (sw2.ElapsedMilliseconds > 200) - { - _logger.InfoFormat("Slow similarity handling, times: {0}/{1}/{2}/{3}", step1, step2, step3, - sw2.ElapsedMilliseconds); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Handlers/GetTagsForIncidentHandler.cs b/src/Server/OneTrueError.App/Modules/Tagging/Handlers/GetTagsForIncidentHandler.cs deleted file mode 100644 index b8e69a90..00000000 --- a/src/Server/OneTrueError.App/Modules/Tagging/Handlers/GetTagsForIncidentHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Modules.Tagging; -using OneTrueError.Api.Modules.Tagging.Queries; -using OneTrueError.App.Modules.Tagging.Domain; - -namespace OneTrueError.App.Modules.Tagging.Handlers -{ - [Component] - internal class GetTagsForIncidentHandler : IQueryHandler - { - private readonly ITagsRepository _repository; - - public GetTagsForIncidentHandler(ITagsRepository repository) - { - _repository = repository; - } - - public async Task ExecuteAsync(GetTagsForIncident query) - { - return (await _repository.GetTagsAsync(query.IncidentId)).Select(ConvertTag).ToArray(); - } - - private TagDTO ConvertTag(Tag arg) - { - return new TagDTO {Name = arg.Name, OrderNumber = arg.OrderNumber}; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Handlers/IdentifyTagsFromIncident.cs b/src/Server/OneTrueError.App/Modules/Tagging/Handlers/IdentifyTagsFromIncident.cs deleted file mode 100644 index 42d6a46b..00000000 --- a/src/Server/OneTrueError.App/Modules/Tagging/Handlers/IdentifyTagsFromIncident.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Incidents.Events; - -namespace OneTrueError.App.Modules.Tagging.Handlers -{ - /// - /// Scan through the error report to identify which libraries were used when the exception was thrown. - /// - [Component(RegisterAsSelf = true)] - public class IdentifyTagsFromIncident : IApplicationEventSubscriber - { - private readonly ILog _logger = LogManager.GetLogger(typeof(IdentifyTagsFromIncident)); - private readonly ITagsRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// repository - public IdentifyTagsFromIncident(ITagsRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - /// Process an event asynchronously. - /// - /// event to process - /// - /// Task to wait on. - /// - public async Task HandleAsync(ReportAddedToIncident e) - { - if (e.Incident.ReportCount != 1) - { - var tags = await _repository.GetTagsAsync(e.Incident.Id); - if (tags.Count > 0) - return; - } - - _logger.Debug("Checking tags.."); - var ctx = new TagIdentifierContext(e.Report); - var identifierProvider = new IdentifierProvider(); - var identifiers = identifierProvider.GetIdentifiers(ctx); - foreach (var identifier in identifiers) - { - identifier.Identify(ctx); - } - - ExtractTagsFromCollections(e, ctx); - - _logger.Debug("done.."); - - await _repository.AddAsync(e.Incident.Id, ctx.Tags.ToArray()); - } - - private void ExtractTagsFromCollections(ReportAddedToIncident e, TagIdentifierContext ctx) - { - foreach (var collection in e.Report.ContextCollections) - { - string tagsStr; - if (!collection.Properties.TryGetValue("OneTrueTags", out tagsStr)) - continue; - - try - { - var tags = tagsStr.Split(','); - foreach (var tag in tags) - { - ctx.AddTag(tag, 1); - } - } - catch (Exception ex) - { - _logger.Error( - "Failed to parse tags from '" + collection.Name + "', invalid tag string: '" + tagsStr + "'.", - ex); - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Tagging/Handlers/IncidentReopenedHandler.cs b/src/Server/OneTrueError.App/Modules/Tagging/Handlers/IncidentReopenedHandler.cs deleted file mode 100644 index fe8c60a3..00000000 --- a/src/Server/OneTrueError.App/Modules/Tagging/Handlers/IncidentReopenedHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Incidents.Events; -using OneTrueError.App.Modules.Tagging.Domain; - -namespace OneTrueError.App.Modules.Tagging.Handlers -{ - /// - /// Adds a "incident-reopened" tag - /// - [Component(RegisterAsSelf = true)] - public class IncidentReopenedHandler : IApplicationEventSubscriber - { - private readonly ITagsRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// repository - public IncidentReopenedHandler(ITagsRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - public async Task HandleAsync(IncidentReOpened e) - { - await _repository.AddAsync(e.IncidentId, new[] {new Tag("incident-reopened", 1)}); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Tagging/ITagsRepository.cs b/src/Server/OneTrueError.App/Modules/Tagging/ITagsRepository.cs deleted file mode 100644 index 1f4359b5..00000000 --- a/src/Server/OneTrueError.App/Modules/Tagging/ITagsRepository.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using OneTrueError.App.Modules.Tagging.Domain; - -namespace OneTrueError.App.Modules.Tagging -{ - /// - /// Repository for tags - /// - public interface ITagsRepository - { - /// - /// Add a new tag - /// - /// incident that the tag is for - /// tag collection - /// task - Task AddAsync(int incidentId, Tag[] tags); - - /// - /// Get a list of tag - /// - /// incident to get tags for - /// List of tags (or an empty list) - [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] - Task> GetTagsAsync(int incidentId); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Tagging/IdentifierProvider.cs b/src/Server/OneTrueError.App/Modules/Tagging/IdentifierProvider.cs deleted file mode 100644 index d9267c42..00000000 --- a/src/Server/OneTrueError.App/Modules/Tagging/IdentifierProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Reflection; - -namespace OneTrueError.App.Modules.Tagging -{ - /// - /// Used to provide tag identifiers (i.e. classes which can identify stackoverflow tags by using the report - /// information) - /// - public class IdentifierProvider - { - private static readonly List Identifiers = new List(); - - [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", - Justification = "How on earth could I do that?")] - static IdentifierProvider() - { - foreach (var type in Assembly.GetExecutingAssembly().GetTypes()) - { - if (type.IsAbstract || type.IsInterface || !typeof(ITagIdentifier).IsAssignableFrom(type)) - continue; - - Identifiers.Add((ITagIdentifier) Activator.CreateInstance(type)); - } - } - - /// - /// Get all identifies for the given context. - /// - /// context - /// all identified identifiers. - /// context - [SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")] - public IEnumerable GetIdentifiers(TagIdentifierContext context) - { - if (context == null) throw new ArgumentNullException("context"); - return Identifiers; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Tagging/TagIdentifierContext.cs b/src/Server/OneTrueError.App/Modules/Tagging/TagIdentifierContext.cs deleted file mode 100644 index d04d8393..00000000 --- a/src/Server/OneTrueError.App/Modules/Tagging/TagIdentifierContext.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using OneTrueError.Api.Core.Reports; -using OneTrueError.App.Modules.Tagging.Domain; - -namespace OneTrueError.App.Modules.Tagging -{ - /// - /// Context used when trying to identify stackoverflow tags - /// - public class TagIdentifierContext - { - private readonly ReportDTO _reportToAnalyze; - private readonly string[] _stacktrace; - private readonly List _tags = new List(); - - - /// - /// Creates a new instance of . - /// - /// rta - /// reportToAnalyze - public TagIdentifierContext(ReportDTO reportToAnalyze) - { - if (reportToAnalyze == null) throw new ArgumentNullException("reportToAnalyze"); - - _reportToAnalyze = reportToAnalyze; - var ex = reportToAnalyze.Exception; - if (ex != null && ex.StackTrace != null) - _stacktrace = ex.StackTrace.Split(new[] {"\r\n"}, StringSplitOptions.RemoveEmptyEntries); - else - _stacktrace = new string[0]; - } - - /// - /// Exception to find tags for. - /// - /// - /// Tags identifying relevant tags which can be used to find information about why the exception happened. Like - /// "EntityFramework", "ASP.NET-MVC" etc. - /// - /// - /// These tags are used directly to search for possible solutions. - /// - public IReadOnlyList Tags - { - get { return _tags; } - } - - /// - /// Add tag if the specified text is found in the stack trace - /// - /// text to find - /// tag to add - /// index in stacktrace if found; otherwise -1 - public int AddIfFound(string libraryToFind, string tagToAdd) - { - if (libraryToFind == null) throw new ArgumentNullException("libraryToFind"); - if (tagToAdd == null) throw new ArgumentNullException("tagToAdd"); - - for (var i = 0; i < _stacktrace.Length; i++) - { - if (_stacktrace[i].IndexOf(libraryToFind, StringComparison.OrdinalIgnoreCase) != -1) - { - AddTag(tagToAdd, i); - return i; - } - } - - return -1; - } - - /// - /// Add a stackoverflow tag - /// - /// tag name - /// used to customize in which order the tags appear on the web page. - public void AddTag(string tag, int orderNumber) - { - if (_tags.Any(x => x.Name == tag)) - return; - _tags.Add(new Tag(tag, orderNumber)); - } - - /// - /// Get a property value from a context collection - /// - /// context collection - /// property in the collection - /// value if found; otherwise null. - public string GetPropertyValue(string collectionName, string propertyName) - { - if (collectionName == null) throw new ArgumentNullException("collectionName"); - if (propertyName == null) throw new ArgumentNullException("propertyName"); - - var assemblies = _reportToAnalyze.ContextCollections.FirstOrDefault(x => x.Name == collectionName); - if (assemblies == null) - return null; - - string version; - return assemblies.Properties.TryGetValue(propertyName, out version) ? version : null; - } - - /// - /// Find a text in the stack trace - /// - /// text to find - /// true if found; otherwise false. - public bool IsFound(string libraryToFind) - { - if (libraryToFind == null) throw new ArgumentNullException("libraryToFind"); - return _stacktrace.Any(t => t.IndexOf(libraryToFind, StringComparison.OrdinalIgnoreCase) != -1); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ActionConfigurationData.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/ActionConfigurationData.cs deleted file mode 100644 index a48a8b5a..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ActionConfigurationData.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// Defines information for a specific action in a trigger. - /// - /// - /// - /// "Send email", for instance, might have email address as . - /// - /// - public class ActionConfigurationData - { - /// - /// Action to take - /// - public string ActionName { get; set; } - - /// - /// Context data for the action. - /// - public string Data { get; set; } - - /// - /// Primary key - /// - public int Id { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/SendEmail.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/SendEmail.cs deleted file mode 100644 index cec90c8c..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Actions/SendEmail.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Threading.Tasks; - -namespace OneTrueError.App.Modules.Triggers.Domain.Actions -{ - [TriggerActionName("Email")] - internal class SendEmailTask : ITriggerAction - { -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - public async Task ExecuteAsync(ActionExecutionContext context) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - //TODO: Create a generic emailer with symbols as the default - // notification system is currently much better than this notification thingy. - //i.e. it do not add any value currently. - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Collections.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/Collections.cs deleted file mode 100644 index e34b0063..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Collections.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; - -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// Contains information about a collection in a specific application - /// - /// - /// - /// This metadata will be used to enable autocomplete when designing triggers for the application. - /// - /// - public class CollectionMetadata - { - /// - /// Creates a new instance of . - /// - /// Application identity (i.e. primary key) - /// Name as specified in the client library - public CollectionMetadata(int applicationId, string name) - { - if (name == null) throw new ArgumentNullException("name"); - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - - Name = name; - ApplicationId = applicationId; - Properties = new List(); - IsUpdated = false; - } - - /// - /// Serialization constructor - /// - protected CollectionMetadata() - { - IsUpdated = false; - } - - /// - /// Application that the incident belongs to - /// - public int ApplicationId { get; private set; } - - /// - /// Collection identity (unique between all collections, while the name is just unique for the referenced incident). - /// - public int Id { get; set; } - - /// - /// Incident has been updated. - /// - [JsonIgnore] - public bool IsUpdated { get; private set; } - - /// - /// Name as specified in the client library - /// - public string Name { get; private set; } - - /// - /// Properties collected by the client library. - /// - public ICollection Properties { get; private set; } - - /// - /// Add or update a property. - /// - /// Property name - public void AddOrUpdateProperty(string name) - { - if (Properties.Any(x => x.Equals(name, StringComparison.OrdinalIgnoreCase))) - return; - - IsUpdated = true; - Properties.Add(name); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterCondition.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterCondition.cs deleted file mode 100644 index 399919b7..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterCondition.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.ComponentModel; - -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// Specifies how the filter value should be compared with the actual property data. - /// - public enum FilterCondition - { - /// - /// Should start with the given value - /// - [Description("Starts with")] StartsWith, - - /// - /// Should end with the given value - /// - [Description("Ends with")] EndsWith, - - /// - /// Should contain the given value - /// - [Description("Contain")] Contains, - - /// - /// Should not contain the given value - /// - [Description("Do not contain")] DoNotContain, - - /// - /// Should equal the given value. - /// - [Description("Equals")] Equals - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterContext.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterContext.cs deleted file mode 100644 index 2360eeab..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -using OneTrueError.Api.Core.Incidents; -using OneTrueError.Api.Core.Reports; - -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// Context filter. - /// - public class FilterContext - { - /// - /// Ges the received error report - /// - public ReportDTO ErrorReport { get; set; } - - /// - /// Gets incident that the report was attached to. - /// - public IncidentSummaryDTO Incident { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterResult.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterResult.cs deleted file mode 100644 index 0e2c6e54..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/FilterResult.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// Result for . - /// - [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterResult")] - public enum FilterResult - { - /// - /// Rule did not match the given conditions. - /// - NotMatched, - - /// - /// Stop processing other rules and grant this report - /// - Grant, - - /// - /// Stop process other rules and revoke this report - /// - Revoke, - - /// - /// Ok by us, pass to any other roles. - /// - Continue - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Filters/NamespaceDoc.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/Filters/NamespaceDoc.cs deleted file mode 100644 index a3f68026..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Filters/NamespaceDoc.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.App.Modules.Triggers.Domain.Filters -{ - /// - /// Innehåller våra fördefinierade filter. - /// - [CompilerGenerated] - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ITriggerRepository.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/ITriggerRepository.cs deleted file mode 100644 index acded130..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ITriggerRepository.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Griffin.Data; - -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// Repository to load information for the Trigger root aggregate. - /// - public interface ITriggerRepository - { - /// - /// Create a new trigger - /// - /// trigger - /// task - Task CreateAsync(Trigger trigger); - - /// - /// Create collection metadata - /// - /// metadata - /// task - Task CreateAsync(CollectionMetadata collection); - - /// - /// Delete a trigger - /// - /// trigger PK - /// task - Task DeleteAsync(int id); - - /// - /// Get a trigger - /// - /// PK - /// trigger - /// Trigger was not found. - Task GetAsync(int id); - - /// - /// Get collection metadata - /// - /// application to load it for. - /// Metadata (or an empty collection) - [SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")] - Task> GetCollectionsAsync(int applicationId); - - /// - /// Get all triggers for the given application - /// - /// app PK - /// - IEnumerable GetForApplication(int applicationId); - - /// - /// Update trigger - /// - /// trigger - /// task - Task UpdateAsync(Trigger entity); - - /// - /// Update metadata - /// - /// collection - /// task - Task UpdateAsync(CollectionMetadata collection); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/LastTriggerAction.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/LastTriggerAction.cs deleted file mode 100644 index 5a5a57b1..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/LastTriggerAction.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// What to do if all filter rules have accepted the report. - /// - public enum LastTriggerAction - { - /// - /// Grant actions execution. - /// - Grant, - - /// - /// Abort. - /// - Revoke - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/ContextCollectionRule.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/ContextCollectionRule.cs deleted file mode 100644 index dd46422b..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/ContextCollectionRule.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Linq; - -namespace OneTrueError.App.Modules.Triggers.Domain.Rules -{ - /// - /// Check a context collection in the trigger - /// - public class ContextCollectionRule : RuleBase, ITriggerRule - { - /// - /// Context collection to check - /// - public string ContextName { get; set; } - - - /// - /// Property in that collection - /// - public string PropertyName { get; set; } - - - /// - /// Value for the property - /// - public string PropertyValue { get; set; } - - - /// - /// Validate report - /// - /// Context info - /// Recommendation - public FilterResult Validate(FilterContext context) - { - if (context == null) throw new ArgumentNullException("context"); - if (string.IsNullOrEmpty(ContextName)) - { - foreach (var ctx in context.ErrorReport.ContextCollections) - { - if (ctx.Properties.Any(property => Matches(PropertyValue, property.Value))) - { - return ResultToUse; - } - } - - return FilterResult.NotMatched; - } - - var errContext = - context.ErrorReport.ContextCollections.FirstOrDefault( - x => x.Name.Equals(ContextName, StringComparison.CurrentCultureIgnoreCase)); - if (errContext == null) - return FilterResult.NotMatched; - - var prop = errContext.Properties.Where( - x => x.Key.Equals(PropertyName, StringComparison.CurrentCultureIgnoreCase)) - .Select(x => x.Value) - .FirstOrDefault(); - - if (prop == null) - return FilterResult.NotMatched; - - return Matches(PropertyValue, prop) ? ResultToUse : FilterResult.NotMatched; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/ExceptionRule.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/ExceptionRule.cs deleted file mode 100644 index 8af15e58..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/ExceptionRule.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; - -namespace OneTrueError.App.Modules.Triggers.Domain.Rules -{ - /// - /// Uses exception details (like Name, Message, StackTrace) to filter the trigger. - /// - public class ExceptionRule : RuleBase, ITriggerRule - { - /// - /// Exception field name - /// - public string FieldName { get; set; } - - /// - /// Value to compare with - /// - public string Value { get; set; } - - /// - /// Validate report - /// - /// Context info - /// Recommendation - public FilterResult Validate(FilterContext context) - { - if (context == null) throw new ArgumentNullException("context"); - if (context.ErrorReport.Exception == null) - return FilterResult.NotMatched; - - bool matches; - switch (FieldName) - { - case "Exception.Name": - matches = Matches(Value, context.ErrorReport.Exception.Name); - break; - case "Exception.Namespace": - matches = Matches(Value, context.ErrorReport.Exception.Namespace); - break; - case "Exception.Assembly": - matches = Matches(Value, context.ErrorReport.Exception.AssemblyName); - break; - case "Exception.StackTrace": - matches = Matches(Value, context.ErrorReport.Exception.StackTrace); - break; - default: - throw new NotSupportedException(FieldName); - } - - - return matches ? ResultToUse : FilterResult.NotMatched; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/RuleBase.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/RuleBase.cs deleted file mode 100644 index 66a59faa..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Rules/RuleBase.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace OneTrueError.App.Modules.Triggers.Domain.Rules -{ - /// - /// Base for trigger rules - /// - public class RuleBase - { - /// - /// How to compare the values - /// - public FilterCondition Condition { get; set; } - - /// - /// Result to use if value comparison succeeds. - /// - public FilterResult ResultToUse { get; set; } - - /// - /// Match - /// - /// first value - /// second value - /// true if values matches according to ; otherwise false. - public bool Matches(string value1, string value2) - { - if (value1 == null) throw new ArgumentNullException("value1"); - - switch (Condition) - { - case FilterCondition.Contains: - return value1.IndexOf(value2, StringComparison.CurrentCultureIgnoreCase) > -1; - case FilterCondition.DoNotContain: - return value1.IndexOf(value2, StringComparison.CurrentCultureIgnoreCase) == -1; - case FilterCondition.EndsWith: - return value1.EndsWith(value2, StringComparison.CurrentCultureIgnoreCase); - case FilterCondition.StartsWith: - return value1.StartsWith(value2, StringComparison.CurrentCultureIgnoreCase); - case FilterCondition.Equals: - return value1.Equals(value2, StringComparison.CurrentCultureIgnoreCase); - default: - throw new NotSupportedException(Condition.ToString()); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ServiceLocatorTriggerActionFactory.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/ServiceLocatorTriggerActionFactory.cs deleted file mode 100644 index f554073f..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/ServiceLocatorTriggerActionFactory.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using Griffin.Container; -using OneTrueError.App.Modules.Triggers.Domain.Actions; - -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// Uses the IoC container to identify trigger actions - /// - [Component] - public class ServiceLocatorTriggerActionFactory : ITriggerActionFactory - { - private static readonly Dictionary _actionTypes = new Dictionary(); - private readonly IServiceLocator _serviceLocator; - - [SuppressMessage("Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", - Justification = "How on earth could I do that?")] - static ServiceLocatorTriggerActionFactory() - { - LoadTypes(Assembly.GetExecutingAssembly()); - } - - /// - /// Creates a new instance of . - /// - /// IoC container - /// serviceLocator - public ServiceLocatorTriggerActionFactory(IServiceLocator serviceLocator) - { - if (serviceLocator == null) throw new ArgumentNullException("serviceLocator"); - _serviceLocator = serviceLocator; - } - - /// - /// Create action - /// - /// Name of the action to create - /// created action - /// Action is not supported - public ITriggerAction Create(string actionName) - { - Type type; - if (!_actionTypes.TryGetValue(actionName, out type)) - throw new NotSupportedException("Do not support action of type " + actionName); - - return (ITriggerAction) _serviceLocator.Resolve(type); - } - - /// - /// Find and load all trigger actions in the specified assembly - /// - /// assembly to search - public static void LoadTypes(Assembly assembly) - { - if (assembly == null) throw new ArgumentNullException("assembly"); - - var types = from t in assembly.GetTypes() - where t.GetCustomAttribute() != null - select new {Type = t, Attrbibute = t.GetCustomAttribute()}; - foreach (var pair in types) - { - _actionTypes.Add(pair.Attrbibute.Name, pair.Type); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Trigger.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/Trigger.cs deleted file mode 100644 index 5744b447..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/Trigger.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// A filter which decides if a notification could be sent. - /// - public class Trigger - { - private List _actions = new List(); - private List _rules = new List(); - - /// - /// Creates a new instance of . - /// - /// application id - /// applicationId - public Trigger(int applicationId) - { - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - ApplicationId = applicationId; - } - - /// - /// Serialization constructor - /// - protected Trigger() - { - } - - /// - /// Actions to take when the rules have been passed. - /// - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Loaded by repos")] - public IEnumerable Actions - { - get { return _actions; } - private set { _actions = new List(value); } - } - - /// - /// Application id - /// - public int ApplicationId { get; set; } - - /// - /// Why the trigger was created and what it does - /// - public string Description { get; set; } - - - /// - /// Identity - /// - public int Id { get; set; } - - /// - /// If no filters match, do this. - /// - public LastTriggerAction LastTriggerAction { get; set; } - - /// - /// Trigger name - /// - public string Name { get; set; } - - /// - /// Rules to check - /// - [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Loaded by repos")] - public IEnumerable Rules - { - get { return _rules; } - private set { _rules = new List(value); } - } - - /// - /// Run when we get a report for an existing incident. - /// - public bool RunForExistingIncidents { get; set; } - - /// - /// Should run for new incidents (receives a new unique exception) - /// - public bool RunForNewIncidents { get; set; } - - /// - /// Run for closed incidents that receive a new report. - /// - public bool RunForReopenedIncidents { get; set; } - - /// - /// Add a new action - /// - /// what to do - public void AddAction(ActionConfigurationData actionData) - { - if (actionData == null) throw new ArgumentNullException("actionData"); - _actions.Add(actionData); - } - - - /// - /// Add the rules in the order that they should be check in. the first rule added is the first rule that will decide - /// which action to take. - /// - /// Rule to add - public void AddRule(ITriggerRule rule) - { - if (rule == null) throw new ArgumentNullException("rule"); - - _rules.Add(rule); - } - - - /// - /// Remove all actions - /// - public void RemoveActions() - { - _actions.Clear(); - } - - /// - /// Remove all rules. - /// - public void RemoveRules() - { - _rules.Clear(); - } - - /// - /// Validate a new incoming report. - /// - /// - public IEnumerable Run(TriggerExecutionContext triggerContext) - { - if (triggerContext == null) throw new ArgumentNullException("triggerContext"); - - var incident = triggerContext.Incident; - var errorReport = triggerContext.ErrorReport; - - var mayRun = RunForNewIncidents && incident.ReportCount == 1 - || RunForExistingIncidents && incident.ReportCount >= 1; - if (!mayRun) - return new ActionConfigurationData[0]; - - - var isGranted = _rules.Count == 0; - var context = new FilterContext {ErrorReport = errorReport, Incident = incident}; - foreach (var rule in _rules) - { - if (rule.Validate(context) == FilterResult.Revoke) - return new ActionConfigurationData[0]; - - if (rule.Validate(context) == FilterResult.Grant) - { - isGranted = true; - break; - } - } - - if (!isGranted && LastTriggerAction == LastTriggerAction.Revoke) - return new ActionConfigurationData[0]; - - - return _actions.ToArray(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Domain/TriggerExecutionContext.cs b/src/Server/OneTrueError.App/Modules/Triggers/Domain/TriggerExecutionContext.cs deleted file mode 100644 index 3d46d734..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Domain/TriggerExecutionContext.cs +++ /dev/null @@ -1,21 +0,0 @@ -using OneTrueError.Api.Core.Incidents; -using OneTrueError.Api.Core.Reports; - -namespace OneTrueError.App.Modules.Triggers.Domain -{ - /// - /// Context providing when the trigger should execute - /// - public class TriggerExecutionContext - { - /// - /// Report that triggered the trigger (ha ha) - /// - public ReportDTO ErrorReport { get; set; } - - /// - /// Incident that the received report belongs to - /// - public IncidentSummaryDTO Incident { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/EventHandlers/UpdateCollectionsOnReportAdded.cs b/src/Server/OneTrueError.App/Modules/Triggers/EventHandlers/UpdateCollectionsOnReportAdded.cs deleted file mode 100644 index d4c21fb3..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/EventHandlers/UpdateCollectionsOnReportAdded.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using log4net; -using OneTrueError.Api.Core.Incidents.Events; -using OneTrueError.App.Modules.Triggers.Domain; - -namespace OneTrueError.App.Modules.Triggers.EventHandlers -{ - /// - /// Responsible of creating context collection metadata for all reports that have been added to an incident. - /// - [Component(RegisterAsSelf = true)] - public class UpdateCollectionsOnReportAdded : IApplicationEventSubscriber - { - private readonly ILog _logger = LogManager.GetLogger(typeof(UpdateCollectionsOnReportAdded)); - private readonly ITriggerRepository _repository; - - /// - /// Creates a new instance of . - /// - /// repos - /// repository - public UpdateCollectionsOnReportAdded(ITriggerRepository repository) - { - if (repository == null) throw new ArgumentNullException("repository"); - _repository = repository; - } - - /// - /// Process an event asynchronously. - /// - /// event to process - /// - /// Task to wait on. - /// - public async Task HandleAsync(ReportAddedToIncident e) - { - if (e == null) throw new ArgumentNullException("e"); - - _logger.Debug("doing collections"); - var collections = await _repository.GetCollectionsAsync(e.Incident.ApplicationId); - foreach (var context in e.Report.ContextCollections) - { - var isNew = false; - var meta = - collections.FirstOrDefault(x => x.Name.Equals(context.Name, StringComparison.OrdinalIgnoreCase)); - if (meta == null) - { - isNew = true; - meta = new CollectionMetadata(e.Incident.ApplicationId, context.Name); - } - - foreach (var property in context.Properties) - { - meta.AddOrUpdateProperty(property.Key); - } - - if (!meta.IsUpdated) - continue; - - if (isNew) - await _repository.CreateAsync(meta); - else - await _repository.UpdateAsync(meta); - } - - _logger.Debug("collections done"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Modules/Triggers/Queries/DomainToDtoConverters.cs b/src/Server/OneTrueError.App/Modules/Triggers/Queries/DomainToDtoConverters.cs deleted file mode 100644 index 0d9ccd4d..00000000 --- a/src/Server/OneTrueError.App/Modules/Triggers/Queries/DomainToDtoConverters.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using OneTrueError.Api.Modules.Triggers; -using OneTrueError.App.Modules.Triggers.Domain; -using OneTrueError.App.Modules.Triggers.Domain.Rules; - -namespace OneTrueError.App.Modules.Triggers.Queries -{ - /// - /// Converts triggers into DTOs which are transferred to the UI - /// - /// - /// - /// Unknown enum values and unknown sub classes should generate exceptions (FormatException) so that we know - /// that the converts have not been updated. - /// - /// - public static class DomainToDtoConverters - { - /// - /// Convert action to a DTO - /// - /// entity - /// dto - public static TriggerActionDataDTO ConvertAction(ActionConfigurationData action) - { - if (action == null) throw new ArgumentNullException("action"); - return new TriggerActionDataDTO - { - ActionContext = action.Data, - ActionName = action.ActionName - }; - } - - /// - /// Convert entity to dto - /// - /// entity - /// dto - /// Unknown enum value in entity - public static TriggerFilterCondition ConvertFilterCondition(FilterCondition filter) - { - switch (filter) - { - case FilterCondition.Contains: - return TriggerFilterCondition.Contains; - case FilterCondition.DoNotContain: - return TriggerFilterCondition.DoNotContain; - case FilterCondition.EndsWith: - return TriggerFilterCondition.EndsWith; - case FilterCondition.Equals: - return TriggerFilterCondition.Equals; - case FilterCondition.StartsWith: - return TriggerFilterCondition.StartsWith; - default: - throw new FormatException(string.Format("Value '{0}' do not exist in the {1} enum.", - filter, typeof(TriggerFilterCondition).Name)); - } - } - - - /// - /// Convert filter - /// - /// entity - /// dto - /// Entity enum contains a value that is currently not handled. - [SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "FilterResult")] - public static TriggerRuleAction ConvertFilterResult(FilterResult ruleAction) - { - switch (ruleAction) - { - case FilterResult.Revoke: - return TriggerRuleAction.AbortTrigger; - case FilterResult.Continue: - return TriggerRuleAction.ContinueWithNextRule; - case FilterResult.Grant: - return TriggerRuleAction.ExecuteActions; - default: - throw new FormatException(string.Format("Value '{0}' do not exist in the {1} enum.", - ruleAction, typeof(TriggerRuleAction).Name)); - } - } - - /// - /// Convert last action - /// - /// entity - /// dto - /// Entity enum value is not recognized. - public static LastTriggerActionDTO ConvertLastAction(LastTriggerAction lastTriggerAction) - { - switch (lastTriggerAction) - { - case LastTriggerAction.Revoke: - return LastTriggerActionDTO.AbortTrigger; - case LastTriggerAction.Grant: - return LastTriggerActionDTO.ExecuteActions; - default: - throw new FormatException(string.Format("Value '{0}' do not exist in the {1} enum.", - lastTriggerAction, typeof(LastTriggerAction).Name)); - } - } - - /// - /// Convert all different types of rules to DTOs - /// - /// entity - /// dto - /// Subclass is not recognized. - public static TriggerRuleBase ConvertRule(ITriggerRule rule) - { - if (rule is ContextCollectionRule) - { - var dto = (ContextCollectionRule) rule; - return new TriggerContextRule - { - ContextName = dto.ContextName, - Filter = ConvertFilterCondition(dto.Condition), - PropertyName = dto.PropertyName, - PropertyValue = dto.PropertyValue, - ResultToUse = ConvertFilterResult(dto.ResultToUse) - }; - } - - if (rule is ExceptionRule) - { - var dto = (ExceptionRule) rule; - return new TriggerExceptionRule - { - FieldName = dto.FieldName, - Filter = ConvertFilterCondition(dto.Condition), - ResultToUse = ConvertFilterResult(dto.ResultToUse), - Value = dto.Value - }; - } - - throw new FormatException("Failed to convert " + rule); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.App/OneTrueError.App.csproj b/src/Server/OneTrueError.App/OneTrueError.App.csproj deleted file mode 100644 index e698e221..00000000 --- a/src/Server/OneTrueError.App/OneTrueError.App.csproj +++ /dev/null @@ -1,298 +0,0 @@ - - - - Debug - AnyCPU - Properties - OneTrueError.App - 512 - Library - {5EF42A74-9323-49FA-A1F6-974D6DE77202} - OneTrueError.App - v4.5.2 - - - ExtendedDesignGuidelineRules.ruleset - true - full - DEBUG;TRACE - bin\Debug\OneTrueError.App.XML - prompt - false - bin\Debug\ - 4 - - - pdbonly - TRACE - prompt - true - bin\Release\ - 4 - - - - OneTrueError.Api - {fc331a95-fca4-4764-8004-0884665dd01f} - - - OneTrueError.Infrastructure - {a78a50da-c9d7-47f2-8528-d7ee39d91924} - - - - - ..\packages\ColorCode.1.0.1\lib\ColorCode.dll - True - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.39\lib\net45\Griffin.Core.dll - True - - - ..\packages\log4net.2.0.5\lib\net45-full\log4net.dll - True - - - ..\packages\MarkdownSharp.1.13.0.0\lib\35\MarkdownSharp.dll - True - - - - ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - ..\packages\UAParser.2.1.0.0\lib\net40-Client\UAParser.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.App/OneTrueError.App.csproj.DotSettings b/src/Server/OneTrueError.App/OneTrueError.App.csproj.DotSettings deleted file mode 100644 index 662f9568..00000000 --- a/src/Server/OneTrueError.App/OneTrueError.App.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - CSharp50 \ No newline at end of file diff --git a/src/Server/OneTrueError.App/OneTrueError.App.csproj.vsspell b/src/Server/OneTrueError.App/OneTrueError.App.csproj.vsspell deleted file mode 100644 index 94043d2a..00000000 --- a/src/Server/OneTrueError.App/OneTrueError.App.csproj.vsspell +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.App/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.App/Properties/AssemblyInfo.cs deleted file mode 100644 index a1a8de9d..00000000 --- a/src/Server/OneTrueError.App/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -[assembly: AssemblyTitle("OneTrueError.App")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.App")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -[assembly: ComVisible(false)] -[assembly: Guid("5ef42a74-9323-49fa-a1f6-974d6de77202")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] -[assembly: InternalsVisibleTo("OneTrueError.SqlServer")] \ No newline at end of file diff --git a/src/Server/OneTrueError.App/packages.config b/src/Server/OneTrueError.App/packages.config deleted file mode 100644 index 55326e87..00000000 --- a/src/Server/OneTrueError.App/packages.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/ConfigFileStore.cs b/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/ConfigFileStore.cs deleted file mode 100644 index b95e628b..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/ConfigFileStore.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Web; -using System.Web.Configuration; - -namespace OneTrueError.Infrastructure.Configuration.ConfigFile -{ - /// - /// Store the configuration in web.config. - /// - public class ConfigFileStore : ConfigurationStore - { - /// - /// Load a settings section - /// - /// Type of section - /// Category if found; otherwise null. - public override T Load() - { - var settingsType = new T(); - var dict = new Dictionary(); - var config = ConfigurationManager.GetSection("oneTrueError") as OneTrueErrorConfigurationSection; - - var section = config.Sections[settingsType.SectionName]; - if (section == null) - return default(T); - - foreach (KeyValueElement elem in section.Settings) - { - dict[elem.Key] = elem.Value; - } - - if (dict.Count == 0) - return default(T); - - settingsType.Load(dict); - return settingsType; - } - - /// - /// Store a settings section. - /// - /// Category to persist. - /// section - /// - /// - /// The section name is used as a prefix for the appSetting key. i.e. the setting Name in a section named - /// Properties would - /// be stored with the key Properties.Name - /// - /// - public override void Store(IConfigurationSection section) - { - if (section == null) throw new ArgumentNullException("section"); - - var configuration = HttpContext.Current == null - ? ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None) - : WebConfigurationManager.OpenWebConfiguration("~/"); - - var config = (OneTrueErrorConfigurationSection) configuration.GetSection("oneTrueError"); - var appConfigSection = config.Sections[section.SectionName]; - if (appConfigSection == null) - { - appConfigSection = new SectionConfigElement {Name = section.SectionName}; - config.Sections.Add(appConfigSection); - } - - var props = section.ToDictionary(); - foreach (var kvp in props) - { - var configItem = appConfigSection.Settings[kvp.Key]; - if (configItem == null) - { - configItem = new KeyValueElement(kvp.Key, kvp.Value); - appConfigSection.Settings.Add(configItem); - } - else - appConfigSection.Settings[kvp.Key].Value = kvp.Value; - } - - - configuration.Save(); - ConfigurationManager.RefreshSection("appSettings"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/KeyValueCollection.cs b/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/KeyValueCollection.cs deleted file mode 100644 index e09ec8ce..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/KeyValueCollection.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Configuration; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.Infrastructure.Configuration.ConfigFile -{ - /// - /// A configuration element used to represent a keu/value collections - /// - [SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")] - public class KeyValueCollection : ConfigurationElementCollection - { - /// - /// Gets the type of the . - /// - /// - /// The of this collection. - /// - public override ConfigurationElementCollectionType CollectionType - { - get { return ConfigurationElementCollectionType.BasicMap; } - } - - /// - /// "setting" - /// - protected override string ElementName - { - get { return "setting"; } - } - - /// - /// Get a key value pair. - /// - /// Key - /// element if found; otherwise null. - public new KeyValueElement this[string key] - { - get - { - if (IndexOf(key) < 0) return null; - return (KeyValueElement) BaseGet(key); - } - } - - /// - /// Get element from index - /// - /// zero based index - /// Element - public KeyValueElement this[int index] - { - get { return (KeyValueElement) BaseGet(index); } - } - - /// - /// Add a new element - /// - /// item - /// item - public void Add(KeyValueElement item) - { - if (item == null) throw new ArgumentNullException("item"); - BaseAdd(item); - } - - /// - /// When overridden in a derived class, creates a new . - /// - /// - /// A newly created . - /// - protected override ConfigurationElement CreateNewElement() - { - return new KeyValueElement(); - } - - /// - /// Gets the element key for a specified configuration element when overridden in a derived class. - /// - /// - /// An that acts as the key for the specified - /// . - /// - /// The to return the key for. - protected override object GetElementKey(ConfigurationElement element) - { - return ((KeyValueElement) element).Key; - } - - - private int IndexOf(string name) - { - name = name.ToLower(); - - for (var idx = 0; idx < Count; idx++) - { - if (this[idx].Key.ToLower() == name) - return idx; - } - return -1; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/KeyValueElement.cs b/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/KeyValueElement.cs deleted file mode 100644 index 3218e554..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/KeyValueElement.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Configuration; - -namespace OneTrueError.Infrastructure.Configuration.ConfigFile -{ - /// - /// Key/value Configuration item - /// - public class KeyValueElement : ConfigurationElement - { - /// - /// Creates a new instance of . - /// - public KeyValueElement() - { - } - - /// - /// Creates a new instance of . - /// - /// key - /// value - /// key; value - public KeyValueElement(string key, string value) - { - if (key == null) throw new ArgumentNullException("key"); - if (value == null) throw new ArgumentNullException("value"); - - Key = key; - Value = value; - } - - /// - /// Key - /// - [ConfigurationProperty("key", IsRequired = true, IsKey = true, DefaultValue = "")] - public string Key - { - get { return (string) this["key"]; } - set { this["key"] = value; } - } - - /// - /// Value - /// - [ConfigurationProperty("value", IsRequired = true)] - public string Value - { - get { return (string) this["value"]; } - set { this["value"] = value; } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/OneTrueErrorConfigurationSection.cs b/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/OneTrueErrorConfigurationSection.cs deleted file mode 100644 index 47215f90..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/OneTrueErrorConfigurationSection.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Configuration; - -namespace OneTrueError.Infrastructure.Configuration.ConfigFile -{ - /// - /// Main configuration section - /// - public class OneTrueErrorConfigurationSection : ConfigurationSection - { - /// - /// Contains all sections for different areas of OneTrueError. - /// - [ConfigurationProperty("", IsDefaultCollection = true)] - public SectionCollection Sections - { - get - { - var hostCollection = (SectionCollection) base[""]; - return hostCollection; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/SectionCollection.cs b/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/SectionCollection.cs deleted file mode 100644 index 5bcc13b5..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/SectionCollection.cs +++ /dev/null @@ -1,167 +0,0 @@ -using System; -using System.Configuration; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.Infrastructure.Configuration.ConfigFile -{ - /// - /// Contains key value pairs - /// - [SuppressMessage("Microsoft.Design", "CA1010:CollectionsShouldImplementGenericInterface")] - public class SectionCollection : ConfigurationElementCollection - { - /// - /// Creates a new instance of . - /// - [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")] - [SuppressMessage("Microsoft.Performance", "CA1820:TestForEmptyStringsUsingStringLength", - Justification = "Value cannot be null.")] - public SectionCollection() - { - var details = (SectionConfigElement) CreateNewElement(); - if (details.Name != "") - { - Add(details); - } - } - - /// - /// Gets the type of the . - /// - /// - /// The of this collection. - /// - public override ConfigurationElementCollectionType CollectionType - { - get { return ConfigurationElementCollectionType.BasicMap; } - } - - /// - /// "section" - /// - protected override string ElementName - { - get { return "section"; } - } - - /// - /// Gives access to a specific section - /// - /// zero based index - public SectionConfigElement this[int index] - { - get { return (SectionConfigElement) BaseGet(index); } - set - { - if (BaseGet(index) != null) - { - BaseRemoveAt(index); - } - BaseAdd(index, value); - } - } - - /// - /// Gives access to a section - /// - /// section name - /// section - public new SectionConfigElement this[string name] - { - get { return (SectionConfigElement) BaseGet(name); } - } - - /// - /// Add a section - /// - /// section - public void Add(SectionConfigElement details) - { - BaseAdd(details); - } - - /// - /// Remove all sections - /// - public void Clear() - { - BaseClear(); - } - - /// - /// Get index of the specified section - /// - /// section - /// index, -1 = not found - public int IndexOf(SectionConfigElement details) - { - return BaseIndexOf(details); - } - - /// - /// Remove section (if found). - /// - /// section - public void Remove(SectionConfigElement details) - { - if (details == null) throw new ArgumentNullException("details"); - if (BaseIndexOf(details) >= 0) - BaseRemove(details.Name); - } - - /// - /// Remove section (if found). - /// - /// section name - public void Remove(string name) - { - if (name == null) throw new ArgumentNullException("name"); - BaseRemove(name); - } - - /// - /// Remove section at the given position - /// - /// zero based index - public void RemoveAt(int index) - { - if (index <= 0) throw new ArgumentOutOfRangeException("index"); - BaseRemoveAt(index); - } - - - /// - /// Adds a configuration element to the . - /// - /// The to add. - protected override void BaseAdd(ConfigurationElement element) - { - if (element == null) throw new ArgumentNullException("element"); - BaseAdd(element, false); - } - - /// - /// When overridden in a derived class, creates a new . - /// - /// - /// A newly created . - /// - protected override ConfigurationElement CreateNewElement() - { - return new SectionConfigElement(); - } - - /// - /// Gets the element key for a specified configuration element when overridden in a derived class. - /// - /// - /// An that acts as the key for the specified - /// . - /// - /// The to return the key for. - protected override object GetElementKey(ConfigurationElement element) - { - return ((SectionConfigElement) element).Name; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/SectionConfigElement.cs b/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/SectionConfigElement.cs deleted file mode 100644 index 29e3b5a0..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/ConfigFile/SectionConfigElement.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Configuration; - -namespace OneTrueError.Infrastructure.Configuration.ConfigFile -{ - /// - /// Configuration element representation a OTE config section - /// - public class SectionConfigElement : ConfigurationElement - { - /// - /// Name of this section - /// - [ConfigurationProperty("name", IsRequired = true, IsKey = true)] - [StringValidator(InvalidCharacters = " ~!@#$%^&*()[]{}/;\"|\\")] - public string Name - { - get { return (string) this["name"]; } - set { this["name"] = value; } - } - - /// - /// Key value collection accessor. - /// - [ConfigurationProperty("", IsDefaultCollection = true)] - public KeyValueCollection Settings - { - get { return (KeyValueCollection) base[""]; } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/ConfigurationCategoryExtensions.cs b/src/Server/OneTrueError.Data.Common/Configuration/ConfigurationCategoryExtensions.cs deleted file mode 100644 index 21f6650d..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/ConfigurationCategoryExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; - -namespace OneTrueError.Infrastructure.Configuration -{ - /// - /// Extensions used to convert between a flat object and a configuration dictionary - /// - public static class ConfigurationCategoryExtensions - { - /// - /// Assign properties from the configuration dictionary. - /// - /// Category that should get its properties assigned - /// Dictionary containing the property values. - /// section;settings - public static void AssignProperties(this IConfigurationSection section, IDictionary settings) - { - if (section == null) throw new ArgumentNullException("section"); - if (settings == null) throw new ArgumentNullException("settings"); - var type = section.GetType(); - foreach (var kvp in settings) - { - var property = type.GetProperty(kvp.Key); - if (property.PropertyType == typeof(Uri)) - { - var value = new Uri(kvp.Value); - property.SetValue(section, value); - } - else if (!property.PropertyType.IsAssignableFrom(typeof(string))) - { - var value = Convert.ChangeType(kvp.Value, property.PropertyType); - property.SetValue(section, value); - } - else - property.SetValue(section, kvp.Value); - } - } - - /// - /// Create a dictionary from the objects properties. - /// - /// Instance to convert - /// Dictionary with all properties (except SectionName) - /// section - public static IDictionary ToConfigDictionary(this IConfigurationSection section) - { - if (section == null) throw new ArgumentNullException("section"); - var items = new Dictionary(); - foreach (var propertyInfo in section.GetType().GetProperties()) - { - if (propertyInfo.Name == "SectionName") - continue; - - var value = propertyInfo.GetValue(section); - items[propertyInfo.Name] = string.Format(CultureInfo.InvariantCulture, "{0}", value); - } - return items; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/ConfigurationStore.cs b/src/Server/OneTrueError.Data.Common/Configuration/ConfigurationStore.cs deleted file mode 100644 index 30aa524f..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/ConfigurationStore.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using OneTrueError.Infrastructure.Configuration.Database; - -namespace OneTrueError.Infrastructure.Configuration -{ - /// - /// Defines how settings should be persisted and loaded. - /// - public abstract class ConfigurationStore - { - /// - /// Used to access the currently configured implementation - /// - [SuppressMessage("Microsoft.Usage", "CA2211:NonConstantFieldsShouldNotBeVisible")] public static - ConfigurationStore Instance = new DatabaseStore(); - - /// - /// Load a settings section - /// - /// Type of section - /// Category if found; otherwise null. - public abstract T Load() where T : IConfigurationSection, new(); - - /// - /// Store a settings section. - /// - /// Category to persist. - /// section - public abstract void Store(IConfigurationSection section); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/Database/DatabaseStore.cs b/src/Server/OneTrueError.Data.Common/Configuration/Database/DatabaseStore.cs deleted file mode 100644 index d1a72801..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/Database/DatabaseStore.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using Griffin.Data; - -namespace OneTrueError.Infrastructure.Configuration.Database -{ - /// - /// Uses a DB to store configuration. - /// - /// - /// - /// Items are cached for 30 seconds to avoid loading the DB. - /// - /// - public class DatabaseStore : ConfigurationStore - { - private readonly Dictionary _items = new Dictionary(); - - /// - /// Load a settings section - /// - /// Type of section - /// Category if found; otherwise null. - public override T Load() - { - lock (_items) - { - Wrapper t; - if (_items.TryGetValue(typeof(T), out t) && !t.HasExpired()) - { - return (T)t.Value; - } - } - - var section = new T(); - using (var connection = ConnectionFactory.Create()) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "SELECT Name, Value FROM Settings WHERE section = @section"; - cmd.AddParameter("section", section.SectionName); - using (var reader = cmd.ExecuteReader()) - { - var items = new Dictionary(); - while (reader.Read()) - { - items[reader.GetString(0)] = reader.GetString(1); - } - - if (items.Count == 0) - return default(T); - - section.Load(items); - } - } - } - - SetCache(section); - return section; - } - - public override void Store(IConfigurationSection section) - { - SetCache(section); - using (var connection = ConnectionFactory.Create()) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = "DELETE FROM Settings WHERE section = @section"; - cmd.AddParameter("section", section.SectionName); - cmd.ExecuteNonQuery(); - } - using (var cmd = connection.CreateCommand()) - { - var index = 0; - foreach (var kvp in section.ToConfigDictionary()) - { - cmd.CommandText += - string.Format( - "INSERT INTO Settings (Section, Name, Value) VALUES(@section, @name{0}, @value{0})", - index); - cmd.AddParameter("name" + index, kvp.Key); - cmd.AddParameter("value" + index, kvp.Value); - ++index; - } - cmd.AddParameter("section", section.SectionName); - cmd.ExecuteNonQuery(); - } - } - } - - private void SetCache(IConfigurationSection section) - { - lock (_items) - { - _items[section.GetType()] = new Wrapper {AddedAtUtc = DateTime.UtcNow, Value = section}; - } - } - - private class Wrapper - { - public DateTime AddedAtUtc { get; set; } - public object Value { get; set; } - - public bool HasExpired() - { - return DateTime.UtcNow.Subtract(AddedAtUtc).TotalSeconds >= 60; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/DictionaryExtensions.cs b/src/Server/OneTrueError.Data.Common/Configuration/DictionaryExtensions.cs deleted file mode 100644 index 69ac902a..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/DictionaryExtensions.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; - -namespace OneTrueError.Infrastructure.Configuration -{ - /// - /// Moves otherwise repeated conversions to a single place. - /// - public static class DictionaryExtensions - { - /// - /// Convert dictionary item to a boolean. - /// - /// instance - /// Key - /// Value - /// If key is not present. Key name is included in the exception message. - /// Value is not a boolean. Includes key name and source value in the exception message. - public static bool GetBoolean(this IDictionary dictionary, string name) - { - if (dictionary == null) throw new ArgumentNullException("dictionary"); - if (name == null) throw new ArgumentNullException("name"); - string value; - if (!dictionary.TryGetValue(name, out value)) - throw new ArgumentException(string.Format("Failed to find key '{0}' in dictionary.", name)); - - bool boolValue; - if (!bool.TryParse(value, out boolValue)) - throw new FormatException(string.Format("Failed to convert '{0}' from value '{1}' to a boolean.", name, - value)); - return boolValue; - } - - /// - /// Convert dictionary item to an integer. - /// - /// instance - /// Key - /// Value - /// If key is not present. Key name is included in the exception message. - /// Value is not a boolean. Includes key name and source value in the exception message. - [SuppressMessage("Microsoft.Naming", "CA1720:IdentifiersShouldNotContainTypeNames", MessageId = "integer")] - public static int GetInteger(this IDictionary dictionary, string name) - { - if (dictionary == null) throw new ArgumentNullException("dictionary"); - if (name == null) throw new ArgumentNullException("name"); - string value; - if (!dictionary.TryGetValue(name, out value)) - throw new ArgumentException(string.Format("Failed to find key '{0}' in dictionary.", name)); - - int intValue; - if (!int.TryParse(value, out intValue)) - throw new FormatException(string.Format("Failed to convert '{0}' from value '{1}' to an integer.", - name, value)); - return intValue; - } - - /// - /// Get a string value. - /// - /// instance - /// Key - /// Value - /// If key is not present. Key name is included in the exception message. - public static string GetString(this IDictionary dictionary, string name) - { - if (dictionary == null) throw new ArgumentNullException("dictionary"); - if (name == null) throw new ArgumentNullException("name"); - string value; - if (!dictionary.TryGetValue(name, out value)) - throw new ArgumentException(string.Format("Failed to find key '{0}' in dictionary.", name)); - - return value; - } - - /// - /// Get a string value. - /// - /// instance - /// Key - /// Value to return if given key is not found. - /// Value if key is found; otherwise given default value. - /// If key is not present. Key name is included in the exception message. - public static string GetString(this IDictionary dictionary, string name, string defaultValue) - { - if (dictionary == null) throw new ArgumentNullException("dictionary"); - if (name == null) throw new ArgumentNullException("name"); - string value; - return !dictionary.TryGetValue(name, out value) ? defaultValue : value; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Configuration/IConfigurationSection.cs b/src/Server/OneTrueError.Data.Common/Configuration/IConfigurationSection.cs deleted file mode 100644 index 571de8b3..00000000 --- a/src/Server/OneTrueError.Data.Common/Configuration/IConfigurationSection.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Collections.Generic; - -namespace OneTrueError.Infrastructure.Configuration -{ - /// - /// Purpose of this interface is to allow strongly types settings to be stored in a configuration store without - /// exposing magic strings. - /// - public interface IConfigurationSection - { - string SectionName { get; } - - void Load(IDictionary settings); - - IDictionary ToDictionary(); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/ConnectionFactory.cs b/src/Server/OneTrueError.Data.Common/ConnectionFactory.cs deleted file mode 100644 index 76bf8189..00000000 --- a/src/Server/OneTrueError.Data.Common/ConnectionFactory.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Configuration; -using System.Data; -using System.Data.Common; - -namespace OneTrueError.Infrastructure -{ - /// - /// Generates SQL connections - /// - public class ConnectionFactory - { - /// - /// Creates a connection using the web.config connection string named Db. - /// - /// open connection - public static IDbConnection Create() - { - var conStr = ConfigurationManager.ConnectionStrings["Db"]; - if (conStr == null) - throw new ConfigurationErrorsException("Expected a named 'Db' in web.config"); - - var provider = DbProviderFactories.GetFactory(conStr.ProviderName); - if (provider == null) - throw new ConfigurationErrorsException($"Sql provider '{conStr.ProviderName}' was not found/registered."); - - var connection = provider.CreateConnection(); - connection.ConnectionString = conStr.ConnectionString; - try - { - connection.Open(); - } - catch (DataException ex) - { - throw new DataException( - $"Failed to connect to '{conStr.ConnectionString}'. See inner exception for the reason.", ex); - } - - return connection; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/IDatabaseUtilities.cs b/src/Server/OneTrueError.Data.Common/IDatabaseUtilities.cs deleted file mode 100644 index cb59ecd4..00000000 --- a/src/Server/OneTrueError.Data.Common/IDatabaseUtilities.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Data; - -namespace OneTrueError.Infrastructure -{ - public interface ISetupDatabaseTools - { - /// - /// Check if the current DB schema is out of date compared to the embedded schema resources. - /// - bool CanSchemaBeUpgraded(); - - /// - /// Update DB schema to latest version. - /// - void UpgradeDatabaseSchema(); - - /// - /// Used to check if the given connection string actually works - /// - /// - /// Something do not work - void CheckConnectionString(string connectionString); - - /// - /// Create all tables in the new DB. - /// - void CreateTables(); - - /// - /// Checks if the tables exists and are for the current DB schema. - /// - bool GotUpToDateTables(); - - /// - /// Open a new connection. - /// - /// Connection - IDbConnection OpenConnection(); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/IncludeNonPublicMembersContractResolver.cs b/src/Server/OneTrueError.Data.Common/IncludeNonPublicMembersContractResolver.cs deleted file mode 100644 index f763bae7..00000000 --- a/src/Server/OneTrueError.Data.Common/IncludeNonPublicMembersContractResolver.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace OneTrueError.Infrastructure -{ - /// - /// Used by JSON.NET to be able to deserialize properties with private setters. - /// - public class IncludeNonPublicMembersContractResolver : DefaultContractResolver - { - //protected override List GetSerializableMembers(Type objectType) - //{ - // var members = base.GetSerializableMembers(objectType); - // return members.Where(m => !m.Name.EndsWith("k__BackingField")).ToList(); - //} - - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - //TODO: Maybe cache - var prop = base.CreateProperty(member, memberSerialization); - - if (!prop.Writable) - { - var property = member as PropertyInfo; - if (property != null) - { - var hasPrivateSetter = property.GetSetMethod(true) != null; - prop.Writable = hasPrivateSetter; - } - } - - return prop; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/OneTrueError.Infrastructure.csproj b/src/Server/OneTrueError.Data.Common/OneTrueError.Infrastructure.csproj deleted file mode 100644 index bb9d21df..00000000 --- a/src/Server/OneTrueError.Data.Common/OneTrueError.Infrastructure.csproj +++ /dev/null @@ -1,106 +0,0 @@ - - - - Debug - AnyCPU - Properties - OneTrueError.Infrastructure - 512 - Library - {A78A50DA-C9D7-47F2-8528-D7EE39D91924} - OneTrueError.Infrastructure - v4.5.2 - - - true - full - DEBUG;TRACE - prompt - false - bin\Debug\ - 4 - - - pdbonly - TRACE - prompt - true - bin\Release\ - 4 - - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.39\lib\net45\Griffin.Core.dll - True - - - - ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/OneTrueSerializer.cs b/src/Server/OneTrueError.Data.Common/OneTrueSerializer.cs deleted file mode 100644 index a03fe548..00000000 --- a/src/Server/OneTrueError.Data.Common/OneTrueSerializer.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System; -using Newtonsoft.Json; - -namespace OneTrueError.Infrastructure -{ - /// - /// Internal serializer, used only to store stuff that aren´t exposed outside the App/data namespace. - /// - public static class OneTrueSerializer - { - private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings - { - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor - }; - - /// - /// Intern - /// - /// - /// - /// - public static T Deserialize(string json) - { - return JsonConvert.DeserializeObject(json, Settings); - } - - /// - /// Deserialize JSON - /// - /// json - /// type being deserialized - /// object - public static object Deserialize(string json, Type type) - { - return JsonConvert.DeserializeObject(json, type, Settings); - } - - /// - /// Serialize to JSON - /// - /// entity - /// JSON - public static string Serialize(object data) - { - return JsonConvert.SerializeObject(data); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.Data.Common/Properties/AssemblyInfo.cs deleted file mode 100644 index 0d397123..00000000 --- a/src/Server/OneTrueError.Data.Common/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("OneTrueError.Data.Common")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.Data.Common")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("a78a50da-c9d7-47f2-8528-d7ee39d91924")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetMessageQueue.cs b/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetMessageQueue.cs deleted file mode 100644 index ab2f3283..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetMessageQueue.cs +++ /dev/null @@ -1,169 +0,0 @@ -using System; -using System.Data; -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Runtime.Serialization.Formatters; -using Griffin.Data; -using Griffin.Data.Mapper; -using Newtonsoft.Json; - -namespace OneTrueError.Infrastructure.Queueing.Ado -{ - public class AdoNetMessageQueue : IMessageQueue - { - private readonly string _connectionString; - - private readonly AdoNetQueueEntryMapper _mapper; - private readonly string _providerName; - private readonly string _queueName; - - private readonly JsonSerializerSettings _settings = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - TypeNameHandling = TypeNameHandling.Auto, - TypeNameAssemblyFormat = FormatterAssemblyStyle.Simple, - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor, - ContractResolver = new IncludeNonPublicMembersContractResolver() - }; - - public AdoNetMessageQueue(string queueName, string providerName, string connectionString) - { - if (queueName == null) throw new ArgumentNullException(nameof(queueName)); - if (providerName == null) throw new ArgumentNullException(nameof(providerName)); - if (connectionString == null) throw new ArgumentNullException(nameof(connectionString)); - if (string.IsNullOrEmpty(queueName)) - throw new ArgumentNullException("queueName"); - - _queueName = queueName; - _providerName = providerName; - _connectionString = connectionString; - _mapper = new AdoNetQueueEntryMapper(); - } - - [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] - public void Write(int applicationId, object message) - { - var json = JsonConvert.SerializeObject(message, _settings); - using (var connection = OpenConnection()) - { - using (var cmd = connection.CreateCommand()) - { - cmd.CommandText = @"INSERT INTO Queue" + _queueName + - @" (CreatedAtUtc, ApplicationId, AssemblyQualifiedTypeName, Body) - VALUES(@CreatedAtUtc, @ApplicationId, @AssemblyQualifiedTypeName, @Body)"; - cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow); - cmd.AddParameter("ApplicationId", applicationId); - cmd.AddParameter("AssemblyQualifiedTypeName", - message.GetType().FullName + ", " + message.GetType().Assembly.GetName().Name); - cmd.AddParameter("body", json); - cmd.ExecuteNonQuery(); - } - } - } - - public IQueueTransaction BeginTransaction() - { - var con = OpenConnection(); - return new AdoNetTransaction(con); - } - - public T Receive() - { - using (var con = OpenConnection()) - { - AdoNetQueueEntry row; - using (var cmd = con.CreateCommand()) - { - cmd.CommandText = @"SELECT TOP(1) * FROM Queue" + _queueName; - row = cmd.FirstOrDefault(_mapper); - } - - if (row == null) - return default(T); - - using (var cmd = con.CreateCommand()) - { - cmd.CommandText = @"DELETE FROM Queue" + _queueName + " WHERE Id = @id"; - cmd.AddParameter("id", row.Id); - cmd.ExecuteNonQuery(); - } - - return JsonConvert.DeserializeObject(row.Body, _settings); - } - } - - public object Receive() - { - using (var con = OpenConnection()) - { - AdoNetQueueEntry row; - using (var cmd = con.CreateCommand()) - { - cmd.CommandText = @"SELECT TOP(1) * FROM Queue" + _queueName; - row = cmd.FirstOrDefault(_mapper); - } - - if (row == null) - return null; - - using (var cmd = con.CreateCommand()) - { - cmd.CommandText = @"DELETE FROM Queue" + _queueName + " WHERE Id = @id"; - cmd.AddParameter("id", row.Id); - cmd.ExecuteNonQuery(); - } - - - var type = Type.GetType(row.AssemblyQualifiedTypeName); - if (type == null) - throw new InvalidOperationException("Could not build a type from '" + row.AssemblyQualifiedTypeName + - "'."); - var body = JsonConvert.DeserializeObject(row.Body, type, _settings); - return body; - } - } - - public T TryReceive(IQueueTransaction transaction, TimeSpan waitTimeout) - { - return Receive(); - } - - public T Receive(IQueueTransaction transaction) - { - var trans = ((AdoNetTransaction) transaction).Transaction; - AdoNetQueueEntry row; - using (var cmd = trans.Connection.CreateCommand()) - { - cmd.Transaction = trans; - cmd.CommandText = @"SELECT TOP(1) * FROM Queue" + _queueName; - row = cmd.FirstOrDefault(_mapper); - } - - if (row == null) - return default(T); - - using (var cmd = trans.Connection.CreateCommand()) - { - cmd.Transaction = trans; - cmd.CommandText = @"DELETE FROM Queue" + _queueName + " WHERE Id = @id"; - cmd.AddParameter("id", row.Id); - cmd.ExecuteNonQuery(); - } - return JsonConvert.DeserializeObject(row.Body); - } - - public T TryReceive(TimeSpan waitTimeout) - { - return Receive(); - } - - private IDbConnection OpenConnection() - { - var connectionFactory = DbProviderFactories.GetFactory(_providerName); - var con = connectionFactory.CreateConnection(); - con.ConnectionString = _connectionString; - con.Open(); - return con; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetQueueEntry.cs b/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetQueueEntry.cs deleted file mode 100644 index b418f32d..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetQueueEntry.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; - -namespace OneTrueError.Infrastructure.Queueing.Ado -{ - public class AdoNetQueueEntry - { - public int ApplicationId { get; set; } - public string AssemblyQualifiedTypeName { get; set; } - - public string Body { get; set; } - public DateTime CreatedAtUtc { get; set; } - public int Id { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetQueueEntryMapper.cs b/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetQueueEntryMapper.cs deleted file mode 100644 index 6aba783f..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetQueueEntryMapper.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Griffin.Data.Mapper; - -namespace OneTrueError.Infrastructure.Queueing.Ado -{ - internal class AdoNetQueueEntryMapper : EntityMapper - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetTransaction.cs b/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetTransaction.cs deleted file mode 100644 index b0cd9944..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/Ado/AdoNetTransaction.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Data; - -namespace OneTrueError.Infrastructure.Queueing.Ado -{ - public sealed class AdoNetTransaction : IQueueTransaction - { - private readonly IDbConnection _con; - - public AdoNetTransaction(IDbConnection con) - { - _con = con; - Transaction = con.BeginTransaction(); - } - - public IDbTransaction Transaction { get; } - - public void Dispose() - { - Transaction.Dispose(); - _con.Dispose(); - } - - public void Rollback() - { - Transaction.Rollback(); - } - - public void Commit() - { - Transaction.Commit(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/IMessageQueue.cs b/src/Server/OneTrueError.Data.Common/Queueing/IMessageQueue.cs deleted file mode 100644 index 8b12f232..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/IMessageQueue.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace OneTrueError.Infrastructure.Queueing -{ - /// - /// Purpose of this class is to allow different queue technologies within OTE. - /// - public interface IMessageQueue - { - IQueueTransaction BeginTransaction(); - - //T Receive(IQueueTransaction transaction); - T Receive(); - object Receive(); - T TryReceive(IQueueTransaction transaction, TimeSpan waitTimeout); - void Write(int applicationId, object message); - //T TryReceive(TimeSpan waitTimeout); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/IMessageQueueProvider.cs b/src/Server/OneTrueError.Data.Common/Queueing/IMessageQueueProvider.cs deleted file mode 100644 index d151b6eb..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/IMessageQueueProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace OneTrueError.Infrastructure.Queueing -{ - /// - /// Used to create an abstraction between the queue implementation and the usage - /// - /// - /// - /// Queues are important to be sure that OTE handles all the work assigned to it, even when it's run as a web site. - /// But since - /// different companies have different level of expertise and requirements we do not want to force a technology - /// upon them. This abstraction - /// therefore give them a choice to choose something that fits them. - /// - /// - /// - /// - public interface IMessageQueueProvider - { - /// - /// Open a queue. - /// - /// queue name. Currently "ReportQueue", "FeedbackQueue" or "EventQueue" - /// queue object which can be treated as a singleton. - IMessageQueue Open(string queueName); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/IQueueTransaction.cs b/src/Server/OneTrueError.Data.Common/Queueing/IQueueTransaction.cs deleted file mode 100644 index b8e2998c..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/IQueueTransaction.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace OneTrueError.Infrastructure.Queueing -{ - public interface IQueueTransaction : IDisposable - { - void Commit(); - void Rollback(); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/MessageQueueSettings.cs b/src/Server/OneTrueError.Data.Common/Queueing/MessageQueueSettings.cs deleted file mode 100644 index 3f399f3e..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/MessageQueueSettings.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Collections.Generic; -using OneTrueError.Infrastructure.Configuration; - -namespace OneTrueError.Infrastructure.Queueing -{ - public sealed class MessageQueueSettings : IConfigurationSection - { - public MessageQueueSettings() - { - UseSql = true; - } - - public bool EventAuthentication { get; set; } - - public string EventQueue { get; set; } - - public bool EventTransactions { get; set; } - - public bool FeedbackAuthentication { get; set; } - - public string FeedbackQueue { get; set; } - - public bool FeedbackTransactions { get; set; } - - public bool ReportAuthentication { get; set; } - - public string ReportQueue { get; set; } - - public bool ReportTransactions { get; set; } - - public bool UseSql { get; set; } - - string IConfigurationSection.SectionName - { - get { return "MessageQueueing"; } - } - - IDictionary IConfigurationSection.ToDictionary() - { - return this.ToConfigDictionary(); - } - - void IConfigurationSection.Load(IDictionary settings) - { - this.AssignProperties(settings); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/Msmq/MsmqMessageQueue.cs b/src/Server/OneTrueError.Data.Common/Queueing/Msmq/MsmqMessageQueue.cs deleted file mode 100644 index 5e8f5018..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/Msmq/MsmqMessageQueue.cs +++ /dev/null @@ -1,213 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Messaging; -using System.Runtime.Serialization.Formatters; -using System.Runtime.Serialization.Formatters.Binary; -using System.Text; -using Newtonsoft.Json; - -namespace OneTrueError.Infrastructure.Queueing.Msmq -{ - public class MsmqMessageQueue : IMessageQueue, IDisposable - { - private readonly bool _useAuthentication; - private readonly bool _useTransactions; - private MessageQueue _queue; - - public MsmqMessageQueue(string queueName, bool useAuthentication, bool useTransactions) - { - if (queueName == null) throw new ArgumentNullException(nameof(queueName)); - - _useAuthentication = useAuthentication; - _useTransactions = useTransactions; - _queue = new MessageQueue(queueName); - _queue.MessageReadPropertyFilter.Extension = true; - _queue.Formatter = null; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// 2 - public void Dispose() - { - Dispose(true); - } - - public T Receive() - { - var message = - _queue.Receive(); - return Deserialize(message); - } - - public object Receive() - { - var msg = _queue.Receive(); - - var reader = new StreamReader(msg.BodyStream, Encoding.UTF8); - var json = reader.ReadToEnd(); - var metadata = MetadataHeader.Deserialize(msg); - var type = Type.GetType(metadata.AssemblyQualifiedTypeName); - if (type == null) - throw new NotSupportedException("Failed to get type class from string '" + - metadata.AssemblyQualifiedTypeName + "'."); - - return OneTrueSerializer.Deserialize(json, type); - } - - public void Write(int applicationId, object message) - { - var msg = new Message {UseAuthentication = _useAuthentication}; - SerializeBody(message, msg); - if (_useTransactions) - _queue.Send(msg, MessageQueueTransactionType.Single); - else - _queue.Send(msg); - } - - public IQueueTransaction BeginTransaction() - { - var trans = new MsmqTransactionAdapter(); - trans.Transaction.Begin(); - return trans; - } - - public T TryReceive(IQueueTransaction transaction, TimeSpan waitTimeout) - { - try - { - var trans = ((MsmqTransactionAdapter) transaction).Transaction; - var message = _queue.Receive(waitTimeout, trans); - return Deserialize(message); - } - catch (MessageQueueException ex) - { - if (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) - return default(T); - throw; - } - } - - public T Receive(IQueueTransaction transaction) - { - try - { - var trans = ((MsmqTransactionAdapter) transaction).Transaction; - var message = _queue.Receive(trans); - return Deserialize(message); - } - catch (MessageQueueException ex) - { - if (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) - return default(T); - throw; - } - } - - public T TryReceive(TimeSpan timeout) - { - try - { - var message = _queue.Receive(timeout); - return Deserialize(message); - } - catch (MessageQueueException ex) - { - if (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) - return default(T); - throw; - } - } - - public void Write(object message) - { - var msg = new Message(); - msg.UseAuthentication = _useAuthentication; - SerializeBody(message, msg); - - if (_useTransactions) - _queue.Send(msg, MessageQueueTransactionType.Single); - else - _queue.Send(msg); - } - - /// - /// Dispose pattern - /// - /// Invoked from Dispose() - protected virtual void Dispose(bool isDisposing) - { - if (_queue != null) - { - _queue.Dispose(); - _queue = null; - } - } - - private T Deserialize(Message message) - { - var reader = new StreamReader(message.BodyStream, Encoding.UTF8); - var json = reader.ReadToEnd(); - return OneTrueSerializer.Deserialize(json); - } - - private static void SerializeBody(object message, Message msg) - { - msg.Extension = new MetadataHeader(message).Serialize(); - var json = JsonConvert.SerializeObject(message); - var buf = Encoding.UTF8.GetBytes(json); - var ms = new MemoryStream(buf, 0, buf.Length); - msg.BodyStream = ms; - } - - [Serializable] - private class MetadataHeader - { - public MetadataHeader(object message) - { - Headers = new Dictionary(); - AssemblyQualifiedTypeName = message.GetType().FullName + ", " + - message.GetType().Assembly.GetName().Name; - } - - - protected MetadataHeader() - { - Headers = new Dictionary(); - } - - public string AssemblyQualifiedTypeName { get; } - - public Dictionary Headers { get; set; } - - public static MetadataHeader Deserialize(Message message) - { - var formatter = CreateFormatter(); - var ms = new MemoryStream(message.Extension); - return (MetadataHeader) formatter.Deserialize(ms); - } - - public byte[] Serialize() - { - var formatter = CreateFormatter(); - var ms = new MemoryStream(); - formatter.Serialize(ms, this); - var buf = new byte[ms.Length]; - ms.Position = 0; - ms.Read(buf, 0, buf.Length); - return buf; - } - - private static BinaryFormatter CreateFormatter() - { - return new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple, - TypeFormat = FormatterTypeStyle.TypesWhenNeeded - }; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/Msmq/MsmqTransactionAdapter.cs b/src/Server/OneTrueError.Data.Common/Queueing/Msmq/MsmqTransactionAdapter.cs deleted file mode 100644 index da118ec0..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/Msmq/MsmqTransactionAdapter.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Messaging; - -namespace OneTrueError.Infrastructure.Queueing.Msmq -{ - public class MsmqTransactionAdapter : IQueueTransaction - { - private bool _haveDisposed; // To detect redundant calls - - public MsmqTransactionAdapter() - { - Transaction = new MessageQueueTransaction(); - } - - public MessageQueueTransaction Transaction { get; } - - public void Commit() - { - Transaction.Commit(); - } - - public void Rollback() - { - Transaction.Abort(); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (_haveDisposed) return; - - if (disposing) - { - Transaction.Dispose(); - } - - _haveDisposed = true; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Queueing/QueueProvider.cs b/src/Server/OneTrueError.Data.Common/Queueing/QueueProvider.cs deleted file mode 100644 index 2c40347f..00000000 --- a/src/Server/OneTrueError.Data.Common/Queueing/QueueProvider.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Configuration; -using Griffin.Container; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Infrastructure.Queueing.Ado; -using OneTrueError.Infrastructure.Queueing.Msmq; - -namespace OneTrueError.Infrastructure.Queueing -{ - /// - /// Purpose of this class is to abstract away the queue creation and coupling to specific implementations (and their - /// life times). - /// - [Component(Lifetime = Lifetime.Singleton)] - public class QueueProvider : IMessageQueueProvider - { - private IMessageQueue _eventQueue; - private IMessageQueue _feedbackQueue; - private IMessageQueue _reportQueue; - - public IMessageQueue Open(string queueName) - { - var config = ConfigurationStore.Instance.Load() - ?? new MessageQueueSettings(); //isn't added by the installation guide. - var conString = ConfigurationManager.ConnectionStrings["Db"].ConnectionString; - var provider = ConfigurationManager.ConnectionStrings["Db"].ProviderName; - switch (queueName) - { - case "ReportQueue": - if (_reportQueue != null) - return _reportQueue; - _reportQueue = config.UseSql - ? (IMessageQueue) new AdoNetMessageQueue("Reports", provider, conString) - : new MsmqMessageQueue(config.ReportQueue, config.ReportAuthentication, - config.ReportTransactions); - return _reportQueue; - case "FeedbackQueue": - if (_feedbackQueue != null) - return _feedbackQueue; - _feedbackQueue = config.UseSql - ? (IMessageQueue) new AdoNetMessageQueue("Feedback", provider, conString) - : new MsmqMessageQueue(config.FeedbackQueue, config.FeedbackAuthentication, - config.FeedbackTransactions); - return _feedbackQueue; - case "EventQueue": - if (_eventQueue != null) - return _eventQueue; - _eventQueue = config.UseSql - ? (IMessageQueue) new AdoNetMessageQueue("Events", provider, conString) - : new MsmqMessageQueue(config.EventQueue, config.EventAuthentication, - config.EventTransactions); - return _eventQueue; - default: - throw new NotSupportedException("Queue is not found: " + queueName); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Security/ClaimsExtensions.cs b/src/Server/OneTrueError.Data.Common/Security/ClaimsExtensions.cs deleted file mode 100644 index 2371aa39..00000000 --- a/src/Server/OneTrueError.Data.Common/Security/ClaimsExtensions.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System; -using System.Linq; -using System.Security.Authentication; -using System.Security.Claims; -using System.Security.Principal; - -namespace OneTrueError.Infrastructure.Security -{ - /// - /// Our OneTrueError specific extensions for claims handling. - /// - public static class ClaimsExtensions - { - /// - /// Mark the current principal so that ASP.NET Identity can get updated (to persist changes over request boundary). - /// - /// current principal - public static void AddUpdateCredentialClaim(this ClaimsPrincipal principal) - { - principal.Identities.First().AddClaim(OneTrueClaims.UpdateIdentity); - } - - /// - /// Throws if returns false. - /// - /// principal to search in - /// application to check - /// true if the claim was found with the given value; otherwise false. - /// Claim is not found in the identity. - public static void EnsureApplicationAdmin(this ClaimsPrincipal principal, int applicationId) - { - if (!IsApplicationAdmin(principal, applicationId)) - throw new UnauthorizedAccessException( - $"User {principal.Identity.Name} is not authorized to manage application {applicationId}."); - } - - /// - /// Get account id (). - /// - /// principal to search in - /// id - /// Claim is not found in the identity/ies. - public static int GetAccountId(this ClaimsPrincipal principal) - { - var claim = principal.FindFirst(x => x.Type == ClaimTypes.NameIdentifier); - if (claim == null) - throw new InvalidOperationException( - "Failed to find ClaimTypes.NameIdentifier, user is probably not logged in."); - - return int.Parse(claim.Value); - } - - /// - /// Get account id (). - /// - /// principal to search in - /// id - /// Claim is not found in the identity/ies. - public static int GetAccountId(this IPrincipal principal) - { - var prince = principal as ClaimsPrincipal; - if (prince == null) - throw new AuthenticationException(principal + " is not a ClaimsPrincipal."); - - var claim = prince.FindFirst(x => x.Type == ClaimTypes.NameIdentifier); - if (claim == null) - throw new InvalidOperationException( - "Failed to find ClaimTypes.NameIdentifier, user is probably not logged in."); - - return int.Parse(claim.Value); - } - - /// - /// Get account id (). - /// - /// identity to search in - /// id - /// Claim is not found in the identity. - public static int GetAccountId(this ClaimsIdentity identity) - { - var claim = identity.FindFirst(x => x.Type == ClaimTypes.NameIdentifier); - if (claim == null) - throw new InvalidOperationException( - "Failed to find ClaimTypes.NameIdentifier, user is probably not logged in."); - - return int.Parse(claim.Value); - } - - /// - /// Checks if the currently logged in user is the same as the given id. - /// - /// Some kind of principal - /// AccountId to compare with. - /// - /// true if current principal is a ClaimsPrincipal, the user is authenticated and the accountId is - /// same; otherwise false. - /// - public static bool IsAccount(this IPrincipal principal, int accountId) - { - var p = principal as ClaimsPrincipal; - if (p == null) - return false; - - if (!p.Identity.IsAuthenticated) - return false; - - return accountId == p.GetAccountId(); - } - - /// - /// Checks if the user has the OneTrueClaims.ApplicationAdmin claim or if user is SysAdmin or System. - /// - /// principal to search in - /// Application to check - /// true if the claim was found with the given value; otherwise false. - /// Claim is not found in the identity. - public static bool IsApplicationAdmin(this ClaimsPrincipal principal, int applicationId) - { - return principal.HasClaim(OneTrueClaims.ApplicationAdmin, applicationId.ToString()) - || principal.IsInRole(OneTrueClaims.RoleSysAdmin) - || principal.IsInRole(OneTrueClaims.RoleSystem); - } - - /// - /// Get if the OneTrueClaims.Application claim is specified for the given application (claim value) - /// - /// principal to search in - /// App to check - /// Check if user is in role SysAdmin or if the user is the System. - /// true if the claim was found with the given value; otherwise false. - /// Claim is not found in the identity. - public static bool IsApplicationMember(this ClaimsPrincipal principal, int applicationId, bool checkSystemRoles = false) - { - var isAdmin = principal.HasClaim(OneTrueClaims.Application, applicationId.ToString()); - if (checkSystemRoles) - isAdmin = isAdmin || IsSysAdmin(principal) || principal.IsInRole(OneTrueClaims.RoleSystem); - return isAdmin; - } - - /// - /// Get if the user is part of OneTrueClaims.RoleSysAdmin. - /// - /// principal to search in - /// true if the role was found; otherwise false. - public static bool IsSysAdmin(this IPrincipal principal) - { - return principal.IsInRole(OneTrueClaims.RoleSysAdmin); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Security/IPrincipalFactory.cs b/src/Server/OneTrueError.Data.Common/Security/IPrincipalFactory.cs deleted file mode 100644 index 699c12d8..00000000 --- a/src/Server/OneTrueError.Data.Common/Security/IPrincipalFactory.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Security.Claims; -using System.Threading.Tasks; - -namespace OneTrueError.Infrastructure.Security -{ - /// - /// Allows us to create a claims principal without a dependency to ASP.NET Identity. - /// - public interface IPrincipalFactory - { - /// - /// Create a new principal. - /// - /// Account information etc - /// Principal - Task CreateAsync(PrincipalFactoryContext context); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Security/OneTrueClaims.cs b/src/Server/OneTrueError.Data.Common/Security/OneTrueClaims.cs deleted file mode 100644 index 17a08485..00000000 --- a/src/Server/OneTrueError.Data.Common/Security/OneTrueClaims.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Security.Claims; - -namespace OneTrueError.Infrastructure.Security -{ - public class OneTrueClaims - { - public const string CurrentApplicationId = "http://onetrueerror.com/claims/currentapplicationid"; - public const string Application = "http://onetrueerror.com/claims/application"; - public const string ApplicationName = "http://onetrueerror.com/claims/application/name"; - public const string ApplicationAdmin = "http://onetrueerror.com/claims/application/admin"; - - public const string RoleSysAdmin = "SysAdmin"; - public const string RoleUser = "SysAdmin"; - public const string RoleSystem = "System"; - - public static readonly Claim UpdateIdentity = new Claim("UpdateIdentity", "true"); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Security/PrincipalFactory.cs b/src/Server/OneTrueError.Data.Common/Security/PrincipalFactory.cs deleted file mode 100644 index 6e3e5e83..00000000 --- a/src/Server/OneTrueError.Data.Common/Security/PrincipalFactory.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Generic; -using System.Configuration; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; - -namespace OneTrueError.Infrastructure.Security -{ - /// - /// Creates the security principal that is used by the system - /// - public class PrincipalFactory : IPrincipalFactory - { - private static readonly IPrincipalFactory _instance = new PrincipalFactory(); - - static PrincipalFactory() - { - //FactoryMethod = context => new OneTruePrincipal(x.); - var factoryType = ConfigurationManager.AppSettings["PrincipalFactoryType"]; - if (factoryType != null) - { - _instance = (IPrincipalFactory)TypeHelper.CreateAssemblyObject(factoryType); - } - } - - Task IPrincipalFactory.CreateAsync(PrincipalFactoryContext context) - { - return CreatePrincipalAsync(context); - } - - /// - /// Create a new principal - /// - /// information that should be stored in the created principal - /// created principal - public static Task CreateAsync(PrincipalFactoryContext context) - { - return _instance.CreateAsync(context); - } - - protected virtual Task CreatePrincipalAsync(PrincipalFactoryContext context) - { - var claims = new List(); - if (context.Claims != null) - claims.AddRange(context.Claims); - if (claims.All(x => x.Type != ClaimTypes.NameIdentifier)) - claims.Add(new Claim(ClaimTypes.NameIdentifier, context.AccountId.ToString(), ClaimValueTypes.String)); - if (claims.All(x => x.Type != ClaimTypes.Name)) - claims.Add(new Claim(ClaimTypes.Name, context.UserName, ClaimValueTypes.String)); - - if (context.Roles != null) - claims.AddRange(context.Roles.Select(role => new Claim(ClaimTypes.Role, role, ClaimValueTypes.String))); - - var identity = new ClaimsIdentity(claims, context.AuthenticationType, ClaimTypes.Name, ClaimTypes.Role); - return Task.FromResult(new ClaimsPrincipal(identity)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/Security/PrincipalFactoryContext.cs b/src/Server/OneTrueError.Data.Common/Security/PrincipalFactoryContext.cs deleted file mode 100644 index 4f671a3f..00000000 --- a/src/Server/OneTrueError.Data.Common/Security/PrincipalFactoryContext.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Security.Claims; - -namespace OneTrueError.Infrastructure.Security -{ - /// - /// Context for the factory method in . - /// - public class PrincipalFactoryContext - { - /// - /// Creates a new instance of . - /// - /// Account, can be 0 = API key - /// user/login name - /// Roles that the user is a member of - public PrincipalFactoryContext(int accountId, string userName, string[] roles) - { - if (userName == null) throw new ArgumentNullException("userName"); - if (roles == null) throw new ArgumentNullException("roles"); - - AccountId = accountId; - UserName = userName; - Roles = roles; - } - - /// - /// Account from the account table - /// - public int AccountId { get; private set; } - - /// - /// Hint to show how the authentication was made. - /// - public string AuthenticationType { get; set; } - - /// - /// Claims for the user. - /// - public Claim[] Claims { get; set; } - - /// - /// Roles that the user is a member of - /// - public string[] Roles { get; private set; } - - /// - /// User/login name - /// - public string UserName { get; private set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/SetupTools.cs b/src/Server/OneTrueError.Data.Common/SetupTools.cs deleted file mode 100644 index 9e4adb63..00000000 --- a/src/Server/OneTrueError.Data.Common/SetupTools.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Messaging; - -namespace OneTrueError.Infrastructure -{ - public static class SetupTools - { - public static ISetupDatabaseTools DbTools { get; set; } - - public static bool ValidateMessageQueue(string queuePath, bool useAuthentication, bool useTransactions, - out string errorMessage) - { - try - { - var queue = new MessageQueue(queuePath); - - var msg = new Message("Hello", new BinaryMessageFormatter()); - msg.UseAuthentication = useAuthentication; - msg.UseDeadLetterQueue = true; - if (useTransactions) - { - queue.Send(msg, MessageQueueTransactionType.Single); - var msg2 = queue.Receive(TimeSpan.FromSeconds(1), MessageQueueTransactionType.Single); - } - else - { - queue.Send(msg, MessageQueueTransactionType.None); - var msg2 = queue.Receive(TimeSpan.FromSeconds(1), MessageQueueTransactionType.None); - } - - errorMessage = ""; - return true; - } - catch (MessageQueueException ex) - { - if (ex.MessageQueueErrorCode == MessageQueueErrorCode.IOTimeout) - { - errorMessage = "'" + queuePath + - "' failed. Reason: Failed to read (timeout). However it can also be that send is incorrectly configured. Check the Dead-letter queue for hints."; - } - else - { - errorMessage = "'" + queuePath + "' failed. Reason: " + ex.Message; - } - return false; - } - catch (Exception ex) - { - errorMessage = "'" + queuePath + "' failed. Reason: " + ex.Message; - return false; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/TypeHelper.cs b/src/Server/OneTrueError.Data.Common/TypeHelper.cs deleted file mode 100644 index 94f6b280..00000000 --- a/src/Server/OneTrueError.Data.Common/TypeHelper.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Linq; - -namespace OneTrueError.Infrastructure -{ - /// - /// Reflection helper. - /// - public class TypeHelper - { - /// - /// Create an instance of an assembly type - /// - /// Type name, assembly name - /// created onstance - /// - /// - /// Can be used when the type name is not a fully qualified assembly name. - /// - /// - public static object CreateAssemblyObject(string typeName) - { - var parts = typeName.Split(',').Select(x => x.Trim()).ToArray(); - var assemblyName = parts[1]; - var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(x => x.GetName().Name.Equals(assemblyName)); - if (asm == null) - throw new InvalidOperationException("Failed to find assembly '" + assemblyName + "'."); - var type = asm.GetType(parts[0]); - if (type == null) - throw new InvalidOperationException("Failed to find type '" + parts[0] + "' in assembly '" + - assemblyName + "'."); - return Activator.CreateInstance(type); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Data.Common/packages.config b/src/Server/OneTrueError.Data.Common/packages.config deleted file mode 100644 index 7c79c25a..00000000 --- a/src/Server/OneTrueError.Data.Common/packages.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/FailedReports/CustomerApplication.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/FailedReports/CustomerApplication.cs deleted file mode 100644 index 14dfa5b8..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/FailedReports/CustomerApplication.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Security.Cryptography; -using System.Text; - -namespace OneTrueError.ReportAnalyzer.Domain.FailedReports -{ - /// - /// Small wrapper around an application - /// - public class CustomerApplication - { - /// - /// Shared secret for the application - /// - public string SharedSecret { get; set; } - - /// - /// Validate received report body - /// - /// signature provided in the HTTP request - /// Compressed body - /// true if HMAC authentication is OK; otherwise false. - public bool ValidateBody(string specifiedSignature, byte[] body) - { - var hashAlgo = new HMACSHA256(Encoding.UTF8.GetBytes(SharedSecret)); - var hash = hashAlgo.ComputeHash(body); - var signature = Convert.ToBase64String(hash); - - // uri encoding :( - specifiedSignature = specifiedSignature.Replace(' ', '+'); - - return specifiedSignature.Equals(signature); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/FailedReports/SaveHandler.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/FailedReports/SaveHandler.cs deleted file mode 100644 index 64af092f..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/FailedReports/SaveHandler.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Data.Common; -using System.Threading.Tasks; -using Griffin.Data; -using log4net; -using Newtonsoft.Json; -using OneTrueError.Infrastructure; -using OneTrueError.ReportAnalyzer.Domain.Reports; -using OneTrueError.ReportAnalyzer.LibContracts; - -namespace OneTrueError.ReportAnalyzer.Domain.FailedReports -{ - /// - /// TODO: Remove or refactor? - /// - internal class SaveReportHandlerOld - { - private readonly ILog _logger = LogManager.GetLogger(typeof(SaveReportHandlerOld)); - private readonly IAdoNetUnitOfWork _unitOfWork; - - public SaveReportHandlerOld(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task BuildReportAsync(string fileId, string appKey, string sig, byte[] reportBody) - { - Guid appKeyGuid; - if (!Guid.TryParse(appKey, out appKeyGuid)) - { - _logger.Warn("Incorrect appKeyFormat: " + appKey + "."); - return true; - } - - - var customer = GetApplication(appKey); - if (customer == null) - { - _logger.Error("Failed to identify appKey " + appKey + "."); - return false; - } - - if (!customer.ValidateBody(sig, reportBody)) - { - _logger.Error("Failed to validate body for " + fileId); - return true; - } - - var report = DeserializeBody(reportBody); - await StoreReport(appKey, "", report); - return true; - } - - - protected CustomerApplication GetApplication(string appKey) - { - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = "SELECT SharedSecret FROM Applications WHERE ApplicationKey = @key"; - cmd.AddParameter("key", appKey); - var secret = (string) cmd.ExecuteScalar(); - return new CustomerApplication - { - SharedSecret = secret - }; - } - } - - private ReceivedReportDTO DeserializeBody(byte[] body) - { - var decompressor = new ReportDecompressor(); - var json = decompressor.Deflate(body); - - return JsonConvert.DeserializeObject(json, - new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.Objects, - ContractResolver = - new IncludeNonPublicMembersContractResolver() - }); - } - - private async Task StoreReport(string appKey, string remoteAddress, ReceivedReportDTO report) - { - try - { - using (var connection = ConnectionFactory.Create()) - { - using (var cmd = (DbCommand) connection.CreateCommand()) - { - cmd.CommandText = - @"INSERT INTO QueueReports (appkey, CreatedAtUtc, RemoteAddress, Body) - VALUES (@appkey, @CreatedAtUtc, @RemoteAddress, @Body);"; - cmd.AddParameter("createdatutc", DateTime.UtcNow); - cmd.AddParameter("appKey", appKey); - cmd.AddParameter("RemoteAddress", remoteAddress); - cmd.AddParameter("Body", JsonConvert.SerializeObject(report)); - await cmd.ExecuteNonQueryAsync(); - } - } - } - catch (Exception ex) - { - _logger.Warn( - "Failed to StoreReport: " + JsonConvert.SerializeObject(new {appKey, model = report}), ex); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Feedback/FeedbackEntity.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/Feedback/FeedbackEntity.cs deleted file mode 100644 index 3b758e17..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Feedback/FeedbackEntity.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; - -namespace OneTrueError.ReportAnalyzer.Domain.Feedback -{ - /// - /// Our feedback entity - /// - public class FeedbackEntity - { - /// - /// Creates a new instance of . - /// - /// Application that the feedback is for - /// Unqiue report id generated by the client library - /// errorId - /// applicationId - public FeedbackEntity(int applicationId, string errorId) - { - if (errorId == null) throw new ArgumentNullException(nameof(errorId)); - if (applicationId <= 0) throw new ArgumentOutOfRangeException(nameof(applicationId)); - - ApplicationId = applicationId; - ErrorId = errorId; - } - - /// - /// Serialization constructor - /// - protected FeedbackEntity() - { - } - - /// - /// Application that the report belongs to - /// - public int ApplicationId { get; private set; } - - /// - /// Email to user (if the user want to receive status updates). - /// - public string Email { get; set; } - - /// - /// Error id generated by the client library - /// - public string ErrorId { get; private set; } - - /// - /// Description written by the user after the exception was detected. - /// - public string Feedback { get; set; } - - /// - /// From where the report was uploaded. - /// - public string RemoteAddress { get; set; } - - /// - /// Report identity (PK in the report table) - /// - public int ReportId { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Incidents/IncidentBeingAnalyzed.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/Incidents/IncidentBeingAnalyzed.cs deleted file mode 100644 index 5b137994..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Incidents/IncidentBeingAnalyzed.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using OneTrueError.ReportAnalyzer.Domain.Reports; - -namespace OneTrueError.ReportAnalyzer.Domain.Incidents -{ - /// - /// Keeps track of all report occurrences for a single incident (i.e. error reports which generates the same hash code) - /// - public class IncidentBeingAnalyzed - { - private string _description; - - /// - /// Creates a new instance of . - /// - protected IncidentBeingAnalyzed() - { - } - - /// - /// Creates a new instance of . - /// - /// - /// entity - /// entity have no hashcode - public IncidentBeingAnalyzed(ErrorReportEntity entity) - { - if (entity == null) throw new ArgumentNullException("entity"); - if (string.IsNullOrEmpty(entity.ReportHashCode)) - throw new ArgumentException("ReportHashCode must be specified to be able to identify duplicates."); - Description = entity.Exception.Message; - FullName = entity.Exception.FullName; - StackTrace = entity.Exception.StackTrace; - - AddReport(entity); - ReportHashCode = entity.ReportHashCode; - HashCodeIdentifier = entity.GenerateHashCodeIdentifier(); - ApplicationId = entity.ApplicationId; - UpdatedAtUtc = entity.CreatedAtUtc; - CreatedAtUtc = entity.CreatedAtUtc; - } - - /// - /// Creates a new instance of . - /// - /// entity - /// exception to analyze - /// entity; exception - /// entity.hashcode is null - public IncidentBeingAnalyzed(ErrorReportEntity entity, ErrorReportException exception) - { - if (entity == null) throw new ArgumentNullException("entity"); - if (exception == null) throw new ArgumentNullException("exception"); - if (string.IsNullOrEmpty(entity.ReportHashCode)) - throw new ArgumentException("ReportHashCode must be specified to be able to identify duplicates."); - - Description = exception.Message; - FullName = exception.FullName; - StackTrace = exception.StackTrace; - - AddReport(entity); - ReportHashCode = entity.ReportHashCode; - HashCodeIdentifier = entity.GenerateHashCodeIdentifier(); - ApplicationId = entity.ApplicationId; - UpdatedAtUtc = entity.CreatedAtUtc; - CreatedAtUtc = entity.CreatedAtUtc; - } - - /// - /// Application that the report belongs in - /// - public int ApplicationId { get; private set; } - - /// - /// When report was created - /// - public DateTime CreatedAtUtc { get; private set; } - - /// - /// Incident description - /// - public string Description - { - get - { - if (string.IsNullOrEmpty(_description)) - return "Ooops Error!"; - - return _description; - } - set { _description = value; } - } - - /// - /// Full name of the exception message. - /// - public string FullName { get; private set; } - - /// - /// Used to identify this incident when the hash code is the same as for other incidents. - /// - /// - public string HashCodeIdentifier { get; private set; } - - /// - /// primary key - /// - public int Id { get; private set; } - - /// - /// Incident is ignored, i.e. do not track any more reports or send any notifications. - /// - public bool IsIgnored { get; set; } - - /// - /// Incident is opened again after being closed. - /// - public bool IsReOpened { get; set; } - - /// - /// Incident have been solved (bug as been identified and corrected) - /// - public bool IsSolved { get; set; } - - /// - /// Set if incident was closed and a solution was written - /// - public DateTime PreviousSolutionAtUtc { get; set; } - - /// - /// When the report was opened again - /// - /// - /// - - public DateTime ReOpenedAtUtc { get; set; } - - /// - /// Total number of reports for this incident (including those who was ignored) - /// - public int ReportCount { get; set; } - - /// - /// Hashcode identifying this incident - /// - public string ReportHashCode { get; private set; } - - /// - /// When the solution was written - /// - public DateTime SolvedAtUtc { get; set; } - - /// - /// Stack trace from the exception - /// - public string StackTrace { get; set; } - - - /// - /// Incident has been updated (received a new report or by an action by a user) - /// - public DateTime UpdatedAtUtc { get; private set; } - - /// - /// Add another report. - /// - /// entity - /// entity - public void AddReport(ErrorReportEntity entity) - { - if (entity == null) throw new ArgumentNullException("entity"); - - if (string.IsNullOrWhiteSpace(StackTrace) && entity.Exception != null) - { - Description = entity.Exception.Message; - FullName = entity.Exception.FullName; - StackTrace = entity.Exception.StackTrace; - } - if (UpdatedAtUtc < entity.CreatedAtUtc) - UpdatedAtUtc = entity.CreatedAtUtc; - - ReportCount++; - } - - /// - /// Open a closed incident. - /// - /// - public void ReOpen() - { - PreviousSolutionAtUtc = SolvedAtUtc; - IsSolved = false; - ReOpenedAtUtc = DateTime.UtcNow; - IsReOpened = true; - } - - /// - /// Just ignored a new report - /// - public void WasJustIgnored() - { - UpdatedAtUtc = DateTime.UtcNow; - ReportCount++; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportContext.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportContext.cs deleted file mode 100644 index 37f958e5..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportContext.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace OneTrueError.ReportAnalyzer.Domain.Reports -{ - /// - /// Context used when analysing the report - /// - public class ErrorReportContext - { - private IDictionary _properties; - - /// - /// Creates a new instance of . - /// - /// context collection name - /// properties for the collection - /// name; properties - public ErrorReportContext(string name, IDictionary properties) - { - if (name == null) throw new ArgumentNullException("name"); - if (properties == null) throw new ArgumentNullException(nameof(properties)); - - Name = name; - Properties = properties; - } - - /// - /// Creates a new instance of . - /// - protected ErrorReportContext() - { - } - - /// - /// Context collection name - /// - public string Name { get; private set; } - - /// - /// Context collection properties - /// - public IDictionary Properties - { - get { return _properties; } - private set - { - if (value == null) - throw new ArgumentNullException("value"); - _properties = value; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportEntity.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportEntity.cs deleted file mode 100644 index 6c2c64d7..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ErrorReportEntity.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using OneTrueError.ReportAnalyzer.Domain.Incidents; - -namespace OneTrueError.ReportAnalyzer.Domain.Reports -{ - /// - /// Represents the incoming error report, unmodified (to allow us to do further processing in the future) - /// - /// - /// - /// Important! The entity suffers from temporal coupling since we do not want to generate the hash code in the - /// receiving web API but in the core windows service. As such - /// the report can't be identified until the windows service has received it from the queue. - /// - /// - public class ErrorReportEntity - { - /// - /// Creates a new instance of . - /// - /// Application that the report belongs to - /// error id generated by the client library - /// when the client library created the report - /// exception - /// context collections - /// - /// - public ErrorReportEntity(int applicationId, string clientReportId, DateTime createdAtUtc, - ErrorReportException exception, IEnumerable contexts) - { - if (clientReportId == null) throw new ArgumentNullException("clientReportId"); - if (exception == null) throw new ArgumentNullException("exception"); - if (contexts == null) throw new ArgumentNullException("contexts"); - if (applicationId <= 0) throw new ArgumentOutOfRangeException("applicationId"); - - ClientReportId = clientReportId; - ApplicationId = applicationId; - CreatedAtUtc = createdAtUtc; - Exception = exception; - //GenerateHashCodeIdentifier(); - ContextInfo = contexts.ToArray(); - } - - /// - /// Serialization constructor - /// - protected ErrorReportEntity() - { - } - - /// - /// PK of the application that this entity is reported for - /// - public int ApplicationId { get; set; } - - /// - /// Used to identify this incident when the hash code is the same as for other incidents. - /// - /// - /// Gets or sets id from the client library - /// - public string ClientReportId { get; private set; } - - /// - /// Context collection - /// - public ErrorReportContext[] ContextInfo { get; private set; } - - /// - /// When this entity was created (in the server) - /// - public DateTime CreatedAtUtc { get; private set; } - - /// - /// Thrown exception - /// - public ErrorReportException Exception { get; set; } - - /// - /// PK - /// - public int Id { get; private set; } - - /// - /// Gets incident that this report belongs to. - /// - public int IncidentId { get; set; } - - /// - /// Remote address from where the report was received. - /// - public string RemoteAddress { get; set; } - - - /// - /// Hash code generated for the exception. - /// - /// - /// Be aware that multiple different incidents (yes, may have the same hash code). - /// - public string ReportHashCode { get; private set; } - - /// - /// Denormalization to be able to generate lists quicker (this is really Exception.Message) - /// - public string Title { get; set; } - - /// - /// Used when we get hash code collisions to identify the correct incident. - /// - public string GenerateHashCodeIdentifier() - { - var identifier = Exception.FullName + "\r\n"; - if (string.IsNullOrEmpty(Exception.StackTrace)) - return identifier; - - var pos = Exception.StackTrace.IndexOf("\r\n", StringComparison.Ordinal); - if (pos != -1) - identifier += Exception.StackTrace.Substring(0, pos); - - return identifier; - } - - /// - /// Temporal coupling, but the only way I could figure out. - /// - /// hashcode used to see if this is an unique exception - /// hashCode - public void Init(string hashCode) - { - if (hashCode == null) throw new ArgumentNullException("hashCode"); - ReportHashCode = hashCode; - } - - /// Returns a string that represents the current object. - /// A string that represents the current object. - /// 2 - public override string ToString() - { - return Exception != null ? Exception.Message : "Exception was not included"; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashCodeGenerator.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashCodeGenerator.cs deleted file mode 100644 index 700ce400..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashCodeGenerator.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Text.RegularExpressions; -using Griffin.Container; - -namespace OneTrueError.ReportAnalyzer.Domain.Reports -{ - /// - /// Used to generate hash codes for incoming error reports - /// - /// - /// This new generator uses context information provided in exceptions to generate the hash code. For instance HTTP 404 - /// exception - /// are based on the URI and not exception information - /// - [Component] - public class HashCodeGenerator - { - private const string RemoveLineNumbersRegEx = @"(:[\w]+ [\d]+)\r\n"; - private readonly IServiceLocator _serviceLocator; - - /// - /// Creates a new instance of . - /// - /// Used to resolve all . - public HashCodeGenerator(IServiceLocator serviceLocator) - { - _serviceLocator = serviceLocator; - } - - /// - /// Generate a new hash code - /// - /// entity - /// hash code - /// entity - public string GenerateHashCode(ErrorReportEntity entity) - { - if (entity == null) throw new ArgumentNullException("entity"); - foreach (var generator in _serviceLocator.ResolveAll()) - { - if (generator.CanGenerateFrom(entity)) - { - // forgiving ones so that we can get the report and process it with a default hash code instead. - var code = generator.GenerateHashCode(entity); - if (code != null) - return code; - } - } - - return DefaultCreateHashCode(entity); - } - - /// - /// Method that will be invoked if no implementations of generates an hash code. - /// - /// received report - /// hash code - /// report - protected virtual string DefaultCreateHashCode(ErrorReportEntity report) - { - if (report == null) throw new ArgumentNullException("report"); - - //TODO: Ta bort radnummers stripparen - var hashSource = report.Exception.FullName + "\r\n"; - hashSource += Regex.Replace(report.Exception.StackTrace, RemoveLineNumbersRegEx, "", RegexOptions.Multiline); - - var hash = 23; - foreach (var c in hashSource) - { - hash = hash*31 + c; - } - return hash.ToString("X"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashcodeGenerators/FileNotFoundHttpErrorGenerator.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashcodeGenerators/FileNotFoundHttpErrorGenerator.cs deleted file mode 100644 index 02a32a99..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/HashcodeGenerators/FileNotFoundHttpErrorGenerator.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System; -using System.Linq; -using Griffin.Container; -using log4net; - -namespace OneTrueError.ReportAnalyzer.Domain.Reports.HashcodeGenerators -{ - /// - /// Generates a hash code based on URLs and status code. - /// - [Component] - public class FileNotFoundHttpErrorGenerator : IHashCodeSubGenerator - { - private readonly ILog _logger = LogManager.GetLogger(typeof(FileNotFoundHttpErrorGenerator)); - - - /// - /// Determines if this instance can generate a hashcode for the given entity. - /// - /// entity to examine - /// true for HttpException; otherwise false. - /// entity - public bool CanGenerateFrom(ErrorReportEntity entity) - { - return entity.Exception.Name == "HttpException" || - entity.Exception.BaseClasses.Any(x => x.EndsWith("HttpException")); - } - - /// - /// Generate a new hash code - /// - /// entity - /// hashcode - /// entity - public string GenerateHashCode(ErrorReportEntity entity) - { - var props = entity.ContextInfo.FirstOrDefault(x => x.Name == "ExceptionProperties") - ?? entity.ContextInfo.FirstOrDefault(x => x.Name == "Properties"); - if (props == null) - { - _logger.Error("Failed to find ExceptionProperties collection for entity " + entity.Id); - return null; - } - - string value; - if (!props.Properties.TryGetValue("HttpCode", out value)) - return null; - var statusCode = int.Parse(value); - - var headerProps = entity.ContextInfo.FirstOrDefault(x => x.Name == "HttpHeaders"); - if (headerProps == null) - { - _logger.Error("Failed to find HttpHeaders collection for entity " + entity.Id); - return null; - } - - var url = headerProps.Properties["Url"].ToLower(); - var pos = url.IndexOf("?"); - if (pos != -1) - url = url.Remove(pos); - - return HashCodeUtility.GetPersistentHashCode(statusCode + "-" + url).ToString("X"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ReportDecompressor.cs b/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ReportDecompressor.cs deleted file mode 100644 index a7f82331..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Domain/Reports/ReportDecompressor.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.IO; -using System.IO.Compression; -using System.Text; - -namespace OneTrueError.ReportAnalyzer.Domain.Reports -{ - /// - /// Decompresses report from GZIP compression - /// - public class ReportDecompressor - { - /// - /// Deflate a compressed error report in JSON format - /// - /// Compressed JSON errorReport - /// JSON string decompressed - public string Deflate(byte[] errorReport) - { - //owned and disposed by decompressor - var zipStream = new MemoryStream(errorReport); - - using (var deflateStream = new MemoryStream()) - { - using (var decompressor = new GZipStream(zipStream, CompressionMode.Decompress)) - { - decompressor.CopyTo(deflateStream); - deflateStream.Position = 0; - var buffer = new byte[deflateStream.Length]; - deflateStream.Read(buffer, 0, (int) deflateStream.Length); - var strBuffer = Encoding.UTF8.GetString(buffer); - return strBuffer; - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/IAnalyticsRepository.cs b/src/Server/OneTrueError.ReportAnalyzer/IAnalyticsRepository.cs deleted file mode 100644 index fbe26647..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/IAnalyticsRepository.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using OneTrueError.ReportAnalyzer.Domain.Incidents; -using OneTrueError.ReportAnalyzer.Domain.Reports; - -namespace OneTrueError.ReportAnalyzer -{ - /// - /// Repository (think CQRS write side in this case. yay!) - /// - public interface IAnalyticsRepository - { - /// - /// Create a new incident - /// - /// incident to persist - /// incentAnalysis - void CreateIncident(IncidentBeingAnalyzed incidentAnalysis); - - /// - /// Create a new error report - /// - /// report to persist - /// report - void CreateReport(ErrorReportEntity report); - - /// - /// There is an incident for the given report error id. - /// - /// error id for a report, generated in the client library - /// true if an incident exists; otherwise false. - /// clientReportId - bool ExistsByClientId(string clientReportId); - - /// - /// Find incident - /// - /// application that the incident belongs to - /// generated hash code - /// - /// Line to use if multiple incidents (typically first line in the exception error - /// message) have the same hash code - /// - /// incident if found; otherwise null. - IncidentBeingAnalyzed FindIncidentForReport(int applicationId, string reportHashCode, string hashCodeIdentifier); - - /// - /// Get application name - /// - /// application id - /// name - string GetAppName(int applicationId); - - /// - /// Update incident - /// - /// incident to persist - /// incentAnalysis - void UpdateIncident(IncidentBeingAnalyzed incidentAnalysis); - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/IncludeNonPublicMembersContractResolver.cs b/src/Server/OneTrueError.ReportAnalyzer/IncludeNonPublicMembersContractResolver.cs deleted file mode 100644 index 3750fbb9..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/IncludeNonPublicMembersContractResolver.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace OneTrueError.ReportAnalyzer -{ - /// - /// Allows us to serialize properties with private setters. - /// - public class IncludeNonPublicMembersContractResolver : DefaultContractResolver - { - /// - /// Creates a for the given - /// . - /// - /// The member's parent . - /// The member to create a for. - /// - /// A created for the given - /// . - /// - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - //TODO: Maybe cache - var prop = base.CreateProperty(member, memberSerialization); - - if (!prop.Writable) - { - var property = member as PropertyInfo; - if (property != null) - { - var hasPrivateSetter = property.GetSetMethod(true) != null; - prop.Writable = hasPrivateSetter; - } - } - - return prop; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/NamespaceDoc.cs b/src/Server/OneTrueError.ReportAnalyzer/LibContracts/NamespaceDoc.cs deleted file mode 100644 index d10dc700..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/NamespaceDoc.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace OneTrueError.ReportAnalyzer.LibContracts -{ - /// - /// These classes are created by the "Receiver" area when new reports are being received. - /// - internal class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedFeedbackDTO.cs b/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedFeedbackDTO.cs deleted file mode 100644 index 970b7d35..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedFeedbackDTO.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; - -namespace OneTrueError.ReportAnalyzer.LibContracts -{ - /// - /// Feedback item as recieved by the client library - /// - [Serializable] - public class ReceivedFeedbackDTO - { - /// - /// Application that the report belongs to. - /// - /// - /// Added when the application is identified in the server. - /// - public int ApplicationId { get; set; } - - /// - /// What the user typed about what he did when the exception occurred. - /// - /// - /// From the library contract - /// - public string Description { get; set; } - - /// - /// User want to get status updates. - /// - /// - /// From the library contract - /// - public string EmailAddress { get; set; } - - /// - /// When the report receiver stored this report - /// - public DateTime ReceivedAtUtc { get; set; } - - /// - /// Remote address that we received the report from - /// - public string RemoteAddress { get; set; } - - /// - /// Unique id for this report. - /// - /// - /// From the library contract - /// - public string ReportId { get; set; } - - /// - /// Version of the report (version of the OneTrueError.Reporting API contract) - /// - public string ReportVersion { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportContextInfo.cs b/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportContextInfo.cs deleted file mode 100644 index 0e15a9b1..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportContextInfo.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace OneTrueError.ReportAnalyzer.LibContracts -{ - /// - /// Context collection - /// - [Serializable] - public class ReceivedReportContextInfo - { - /// - /// Creates a new instance of . - /// - protected ReceivedReportContextInfo() - { - } - - /// - /// Creates a new instance of . - /// - /// context collection name - /// properties - /// name; items - public ReceivedReportContextInfo(string name, Dictionary properties) - { - if (name == null) throw new ArgumentNullException("name"); - if (properties == null) throw new ArgumentNullException("properties"); - - Name = name; - Properties = properties; - } - - - /// - /// Context collection name - /// - public string Name { get; set; } - - /// - /// Properties - /// - public Dictionary Properties { get; set; } - - /// - /// Returns a string that represents the current object. - /// - /// - /// A string that represents the current object. - /// - /// 2 - public override string ToString() - { - return Name + " [" + string.Join(", ", - Properties.Select(x => x.Key + "=" + x.Value)) + "]"; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportDTO.cs b/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportDTO.cs deleted file mode 100644 index 5ce1cea4..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportDTO.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; - -namespace OneTrueError.ReportAnalyzer.LibContracts -{ - /// - /// DTO for a client library report. Should always look exactly as the report in the OneTrueError.Client assembly - /// - [Serializable] - public class ReceivedReportDTO - { - /// - /// Application that this report belongs to - /// - public int ApplicationId { get; set; } - - /// - /// A collection of context information such as HTTP request information or computer hardware info. - /// - public ReceivedReportContextInfo[] ContextCollections { get; set; } - - /// - /// Date specified at client side - /// - public DateTime CreatedAtUtc { get; set; } - - /// - /// When the report receiver stored this report - /// - public DateTime DateReceivedUtc { get; set; } - - - /// - /// Exception which was caught. - /// - public ReceivedReportException Exception { get; set; } - - /// - /// Remote address that we received the report from - /// - public string RemoteAddress { get; set; } - - /// - /// Gets incident id (unique identifier used in communication with the customer to identify this error) - /// - public string ReportId { get; set; } - - /// - /// Version of the report (version of the OneTrueError.Reporting API contract) - /// - public string ReportVersion { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportException.cs b/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportException.cs deleted file mode 100644 index a7c83854..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/LibContracts/ReceivedReportException.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace OneTrueError.ReportAnalyzer.LibContracts -{ - /// - /// Model used to wrap all information from an exception. - /// - [Serializable] - public class ReceivedReportException - { - /// - /// Initializes a new instance of the class. - /// - public ReceivedReportException() - { - Properties = new Dictionary(); - } - - /// - /// Assembly name (version included) - /// - public string AssemblyName { get; set; } - - /// - /// Exception base classes. Most specific first: ArgumentOutOfRangeException, ArgumentException, - /// Exception. - /// - public string[] BaseClasses { get; set; } - - /// - /// Everything (exception.ToString()) - /// - public string Everything { get; set; } - - /// - /// Full type name (namespace + class name) - /// - public string FullName { get; set; } - - /// - /// Inner exception (if any; otherwise null). - /// - public ReceivedReportException InnerException { get; set; } - - /// - /// Exception message - /// - public string Message { get; set; } - - /// - /// Type name - /// - public string Name { get; set; } - - /// - /// Namespace that the exception is in - /// - public string Namespace { get; set; } - - /// - /// All properties (public and private) - /// - public IDictionary Properties { get; set; } - - /// - /// Stack trace, line numbers included if your app also distributes the PDB files. - /// - public string StackTrace { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/OneTrueError.ReportAnalyzer.csproj b/src/Server/OneTrueError.ReportAnalyzer/OneTrueError.ReportAnalyzer.csproj deleted file mode 100644 index 4541fb53..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/OneTrueError.ReportAnalyzer.csproj +++ /dev/null @@ -1,123 +0,0 @@ - - - - - Debug - AnyCPU - {29FBF805-CAFD-426A-A576-9756D375BF18} - Library - Properties - OneTrueError.ReportAnalyzer - OneTrueError.ReportAnalyzer - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\OneTrueError.ReportAnalyzer.XML - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.39\lib\net45\Griffin.Core.dll - True - - - ..\packages\Griffin.Framework.Json.1.0.2\lib\net45\Griffin.Core.Json.dll - True - - - ..\packages\log4net.2.0.5\lib\net45-full\log4net.dll - True - - - ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {FC331A95-FCA4-4764-8004-0884665DD01F} - OneTrueError.Api - - - {A78A50DA-C9D7-47F2-8528-D7EE39D91924} - OneTrueError.Infrastructure - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/OneTrueError.ReportAnalyzer.csproj.DotSettings b/src/Server/OneTrueError.ReportAnalyzer/OneTrueError.ReportAnalyzer.csproj.DotSettings deleted file mode 100644 index 662f9568..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/OneTrueError.ReportAnalyzer.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - CSharp50 \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/OneTruePrincipal.cs b/src/Server/OneTrueError.ReportAnalyzer/OneTruePrincipal.cs deleted file mode 100644 index 82f0a0a1..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/OneTruePrincipal.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System; -using System.Security.Principal; -using System.Threading; - -namespace OneTrueError.ReportAnalyzer -{ - /// - /// OneTrueError principal - /// - public class OneTruePrincipal : IPrincipal - { - /// - /// Creates a new instance of . - /// - /// logged in user - /// userName - public OneTruePrincipal(string userName) - { - if (userName == null) throw new ArgumentNullException(nameof(userName)); - Identity = new GenericIdentity(userName); - } - - /// - /// Current user, do not work in an async/task context. - /// - public static OneTruePrincipal Current - { - get { return (OneTruePrincipal) Thread.CurrentPrincipal; } - } - - - /// - /// Determines whether the current principal belongs to the specified role. - /// - /// - /// true if the current principal is a member of the specified role; otherwise, false. - /// - /// The name of the role for which to check membership. - public bool IsInRole(string role) - { - throw new NotImplementedException(); - } - - /// - /// Gets the identity of the current principal. - /// - /// - /// The object associated with the current principal. - /// - public IIdentity Identity { get; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.ReportAnalyzer/Properties/AssemblyInfo.cs deleted file mode 100644 index f4ffca16..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("OneTrueError.ReportAnalyzer")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.ReportAnalyzer")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("29fbf805-cafd-426a-a576-9756d375bf18")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Scanners/ApplicationDTO.cs b/src/Server/OneTrueError.ReportAnalyzer/Scanners/ApplicationDTO.cs deleted file mode 100644 index c1f20e35..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Scanners/ApplicationDTO.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace OneTrueError.ReportAnalyzer.Scanners -{ - /// - /// Application - /// - public class ApplicationDTO - { - /// - /// Identity - /// - public int ApplicationId { get; set; } - - /// - /// Application key - /// - public string ApplicationKey { get; set; } - - /// - /// Shared key (used to sign the uploaded reports) - /// - public string SharedSecret { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Scanners/LoadReportsFromDiskQueueFolder.cs b/src/Server/OneTrueError.ReportAnalyzer/Scanners/LoadReportsFromDiskQueueFolder.cs deleted file mode 100644 index 54d563af..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Scanners/LoadReportsFromDiskQueueFolder.cs +++ /dev/null @@ -1,123 +0,0 @@ -//TODO: Create MSMQ analyzer. -//using System; -//using System.Collections.Generic; -//using System.Configuration; -//using System.Data; -//using System.Data.Common; -//using System.IO; -//using System.Linq; -//using System.Threading; -//using Griffin.ApplicationServices; -//using Griffin.Container; -//using Griffin.Data; -//using OneTrueError.MicroService.Core.Authentication; -//using OneTrueError.ReportAnalyzer.App.Services; -//using OneTrueError.Reporting.Contracts; - -//namespace OneTrueError.ReportAnalyzer.App.Scanners -//{ -// [Component(Lifetime = Lifetime.Singleton)] -// internal class LoadReportsFromDiskQueueFolder : ApplicationServiceTimer -// { -// private readonly ReportDtoConverter _reportDtoConverter = new ReportDtoConverter(); -// private readonly IScopedTaskInvoker _scopedTaskInvoker; -// private List _applications = new List(); - -// public LoadReportsFromDiskQueueFolder(IScopedTaskInvoker scopedTaskInvoker) -// { -// _scopedTaskInvoker = scopedTaskInvoker; -// } - -// protected override void Execute() -// { -// var reader = new FileReader(); -// Dictionary headers; -// string json; -// LoadApplicationKeys(); -// while (reader.ReadNextFile(out headers, out json)) -// { -// ErrorReportDTO report; -// try -// { -// report = _reportDtoConverter.LoadReportFromJson(json); -// } -// catch (Exception) -// { -// var path = ConfigurationManager.AppSettings["ReportStoragePath"] ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Reports"); -// var folder = Path.Combine(path, "InvalidFormat"); -// File.WriteAllText(Path.Combine(folder, Guid.NewGuid().ToString() + ".json"), json); -// throw; -// } - - -// var app = _applications.FirstOrDefault(x => x.ApplicationKey.Equals((string)headers["ApplicationKey"], StringComparison.OrdinalIgnoreCase)); -// if (app == null) -// { -// continue; -// //TODO: FAIL -// } - -// var newReport = _reportDtoConverter.ConvertReport(report, app.ApplicationId); - -// if (headers.ContainsKey("RemoteAddress")) -// newReport.RemoteAddress = (string) headers["RemoteAddress"]; - -// var principal = new OneTruePrincipal(app.CustomerId, "Service"); -// Thread.CurrentPrincipal = principal; -// _scopedTaskInvoker.Execute(x => { x.Analyze(newReport); }); -// } -// } - -// private void LoadApplicationKeys() -// { -// using (var connection = OpenConnection("ReportsDB")) -// using (var transaction = connection.BeginTransaction()) -// using (var cmd = connection.CreateCommand()) -// { -// cmd.Transaction = transaction; -// cmd.CommandText = @"SELECT CustomerId, ApplicationKey, SharedSecret, ApplicationId -// FROM CustomerApplications"; - -// try -// { -// using (var reader = cmd.ExecuteReader()) -// { -// var applications = new List(); -// while (reader.Read()) -// { -// var app = new CustomerApp -// { -// ApplicationId = (int) reader["ApplicationId"], -// CustomerId = (int) reader["CustomerId"], -// ApplicationKey = (string) reader["ApplicationKey"] -// }; -// applications.Add(app); -// } -// _applications = applications; -// } -// } -// catch (Exception ex) -// { -// throw cmd.CreateDataException(ex); -// } -// } -// } - -// private IDbConnection OpenConnection(string name) -// { -// var conStr = ConfigurationManager.ConnectionStrings[name]; -// if (conStr == null) -// throw new ConfigurationErrorsException("Failed to find connectionString '" + name + "'."); -// var provider = DbProviderFactories.GetFactory(conStr.ProviderName); -// if (provider == null) -// throw new ConfigurationErrorsException("Failed to find DbProviderFactory '" + conStr.ProviderName + "'."); - -// var connection = provider.CreateConnection(); -// connection.ConnectionString = conStr.ConnectionString; -// connection.Open(); -// return connection; -// } - -// } -//} - diff --git a/src/Server/OneTrueError.ReportAnalyzer/Scanners/ReportDtoConverter.cs b/src/Server/OneTrueError.ReportAnalyzer/Scanners/ReportDtoConverter.cs deleted file mode 100644 index 69053d95..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Scanners/ReportDtoConverter.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.Linq; -using Newtonsoft.Json; -using OneTrueError.ReportAnalyzer.Domain.Reports; -using OneTrueError.ReportAnalyzer.LibContracts; - -namespace OneTrueError.ReportAnalyzer.Scanners -{ - /// - /// Converts DTOs from the client library format to our internal DTO. - /// - public class ReportDtoConverter - { - /// - /// Convert exception to our internal format - /// - /// exception - /// our format - public ErrorReportException ConvertException(ReceivedReportException exception) - { - var ex = new ErrorReportException - { - AssemblyName = exception.AssemblyName, - BaseClasses = exception.BaseClasses, - Everything = exception.Everything, - FullName = exception.FullName, - Message = exception.Message, - Name = exception.Name, - Namespace = exception.Namespace, - StackTrace = exception.StackTrace - }; - if (exception.InnerException != null) - ex.InnerException = ConvertException(exception.InnerException); - return ex; - } - - /// - /// Convert received report to our internal format - /// - /// client report - /// application that we identified that the report belongs to - /// internal format - public ErrorReportEntity ConvertReport(ReceivedReportDTO report, int applicationId) - { - ErrorReportException ex = null; - if (report.Exception != null) - { - ex = ConvertException(report.Exception); - } - - //var id = _idGeneratorClient.GetNextId(ErrorReportEntity.SEQUENCE); - var contexts = report.ContextCollections.Select(x => new ErrorReportContext(x.Name, x.Properties)).ToArray(); - var dto = new ErrorReportEntity(applicationId, report.ReportId, report.CreatedAtUtc, ex, contexts); - return dto; - } - - /// - /// Deserialize a client library formatted report - /// - /// JSON - /// DTO - public ReceivedReportDTO LoadReportFromJson(string json) - { - var report = - JsonConvert.DeserializeObject(json, new JsonSerializerSettings - { - ObjectCreationHandling = ObjectCreationHandling.Auto, - TypeNameHandling = TypeNameHandling.Auto, - ContractResolver = new IncludeNonPublicMembersContractResolver(), - ConstructorHandling = ConstructorHandling.AllowNonPublicDefaultConstructor - }); - return report; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Scanners/ScanForNewErrorReports.cs b/src/Server/OneTrueError.ReportAnalyzer/Scanners/ScanForNewErrorReports.cs deleted file mode 100644 index 7c9b26de..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Scanners/ScanForNewErrorReports.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Linq; -using Griffin.Container; -using log4net; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.ReportAnalyzer.Domain.Reports; -using OneTrueError.ReportAnalyzer.LibContracts; - -namespace OneTrueError.ReportAnalyzer.Scanners -{ - /// - /// Loads a set of reports that should be analyzed and then cast some wizardry on them. - /// - [Component] - public class ScanForNewErrorReports - { - private readonly Services.ReportAnalyzer _analyzer; - private readonly ILog _logger = LogManager.GetLogger(typeof(ScanForNewErrorReports)); - private readonly IMessageQueue _queue; - - /// - /// Creates a new instance of . - /// - /// to analyze inbound reprots - /// To be able to read inbound reports - public ScanForNewErrorReports(Services.ReportAnalyzer analyzer, - IMessageQueueProvider queueProvider) - { - _analyzer = analyzer; - _queue = queueProvider.Open("ReportQueue"); - } - - - /// - /// Execute on a set of report mastery. - /// - /// false if there are no more reports to analyze. - public bool Execute() - { - using (var transaction = _queue.BeginTransaction()) - { - var dto = _queue.TryReceive(transaction, TimeSpan.FromMilliseconds(500)); - if (dto == null) - return false; - - try - { - ErrorReportException ex = null; - if (dto.Exception != null) - { - ex = ConvertException(dto.Exception); - } - var contexts = dto.ContextCollections.Select(ConvertContext).ToArray(); - var entity = new ErrorReportEntity(dto.ApplicationId, dto.ReportId, dto.CreatedAtUtc, ex, contexts) - { - RemoteAddress = dto.RemoteAddress - }; - _analyzer.Analyze(entity); - } - catch (Exception ex) - { - _logger.Error("Failed to analyze report ", ex); - } - - transaction.Commit(); - } - return true; - } - - private ErrorReportContext ConvertContext(ReceivedReportContextInfo arg) - { - return new ErrorReportContext(arg.Name, arg.Properties); - } - - private ErrorReportException ConvertException(ReceivedReportException dto) - { - var entity = new ErrorReportException - { - Message = dto.Message, - FullName = dto.FullName, - Name = dto.Name, - AssemblyName = dto.AssemblyName, - BaseClasses = dto.BaseClasses, - Everything = dto.Everything, - Namespace = dto.Namespace, - StackTrace = dto.StackTrace - }; - if (dto.InnerException != null) - entity.InnerException = ConvertException(dto.InnerException); - return entity; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Scanners/ScanForNewErrorReportsBatcher.cs b/src/Server/OneTrueError.ReportAnalyzer/Scanners/ScanForNewErrorReportsBatcher.cs deleted file mode 100644 index b5343247..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Scanners/ScanForNewErrorReportsBatcher.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using Griffin.ApplicationServices; -using Griffin.Container; -using log4net; - -namespace OneTrueError.ReportAnalyzer.Scanners -{ - /// - /// Executes from a background thread. - /// - [Component(Lifetime = Lifetime.Singleton)] - public class ScanForNewErrorReportsBatcher : ApplicationServiceTimer - { - private readonly IScopedTaskInvoker _invoker; - private ILog _logger = LogManager.GetLogger(typeof(ScanForNewErrorReportsBatcher)); - - /// - /// Creates a new instance of . - /// - /// Creates a IoC lifetime scope everytime is executed. - /// invoker - public ScanForNewErrorReportsBatcher(IScopedTaskInvoker invoker) - { - if (invoker == null) throw new ArgumentNullException(nameof(invoker)); - _invoker = invoker; - } - - /// - /// Used to do work periodically. - /// - /// - /// Invoked every time the timer does an iteration. The interval is configured by - /// and - /// . The intervals - /// are paused during the execution of Execute() so that your method is not invoked twice if it doesn't complete - /// within the specified interval. - /// - /// - /// - /// protected override void Execute() - /// { - /// //Do some work. - /// } - /// - /// - protected override void Execute() - { - while (true) - { - // There is no spoon. - var fork = false; - _invoker.Execute(x => fork = x.Execute()); - if (false == fork) - break; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/Services/ReportAnalyzer.cs b/src/Server/OneTrueError.ReportAnalyzer/Services/ReportAnalyzer.cs deleted file mode 100644 index f215bb7b..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/Services/ReportAnalyzer.cs +++ /dev/null @@ -1,216 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using DotNetCqs; -using Griffin.Container; -using log4net; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using OneTrueError.Api.Core.Incidents; -using OneTrueError.Api.Core.Incidents.Events; -using OneTrueError.Api.Core.Reports; -using OneTrueError.ReportAnalyzer.Domain.Incidents; -using OneTrueError.ReportAnalyzer.Domain.Reports; - -namespace OneTrueError.ReportAnalyzer.Services -{ - /// - /// Runs analysis for the report. - /// - [Component(Lifetime = Lifetime.Scoped)] - public class ReportAnalyzer - { - private readonly IEventBus _eventBus; - private readonly HashCodeGenerator _hashCodeGenerator; - private readonly ILog _logger = LogManager.GetLogger(typeof(ReportAnalyzer)); - private readonly IAnalyticsRepository _repository; - - /// - /// Creates a new instance of . - /// - /// Used to identify is this is a new unique exception - /// to publish the event - /// repos - public ReportAnalyzer(HashCodeGenerator hashCodeGenerator, IEventBus eventBus, IAnalyticsRepository repository) - { - _hashCodeGenerator = hashCodeGenerator; - _eventBus = eventBus; - _repository = repository; - } - - /// - /// Analyze report - /// - /// report - /// report - public void Analyze(ErrorReportEntity report) - { - if (report == null) throw new ArgumentNullException("report"); - - var exists = _repository.ExistsByClientId(report.ClientReportId); - if (exists) - { - _logger.Warn("Report have already been uploaded: " + report.ClientReportId); - return; - } - - try - { - report.Init(_hashCodeGenerator.GenerateHashCode(report)); - } - catch (Exception ex) - { - _logger.Fatal("Failed to store report " + JsonConvert.SerializeObject(report), ex); - return; - } - - var isReOpened = false; - var incident = _repository.FindIncidentForReport(report.ApplicationId, report.ReportHashCode, - report.GenerateHashCodeIdentifier()); - - - if (incident == null) - { - incident = BuildIncident(report); - _repository.CreateIncident(incident); - } - else - { - if (incident.ReportCount > 10000) - { - _logger.Debug("Reportcount is more than 10000. Ignoring report for incident " + incident.Id); - return; - } - - if (incident.IsIgnored) - { - _logger.Info("Incident is ignored: " + JsonConvert.SerializeObject(report)); - incident.WasJustIgnored(); - _repository.UpdateIncident(incident); - return; - } - if (incident.IsSolved) - { - isReOpened = true; - incident.ReOpen(); - _eventBus.PublishAsync(new IncidentReOpened(incident.ApplicationId, incident.Id, - incident.CreatedAtUtc)); - } - - incident.AddReport(report); - _repository.UpdateIncident(incident); - } - - report.IncidentId = incident.Id; - _repository.CreateReport(report); - _logger.Debug("saving report " + report.Id + " for incident " + incident.Id); - var appName = _repository.GetAppName(incident.ApplicationId); - - var summary = new IncidentSummaryDTO(incident.Id, incident.Description) - { - ApplicationId = incident.ApplicationId, - ApplicationName = appName, - CreatedAtUtc = incident.CreatedAtUtc, - LastUpdateAtUtc = incident.UpdatedAtUtc, - IsReOpened = incident.IsReOpened, - Name = incident.Description, - ReportCount = incident.ReportCount - }; - var sw = new Stopwatch(); - sw.Start(); - _logger.Debug("Publishing now: " + report.ClientReportId); - var e = new ReportAddedToIncident(summary, ConvertToCoreReport(report), isReOpened); - _eventBus.PublishAsync(e); - if (sw.ElapsedMilliseconds > 200) - _logger.Debug("Publish took " + sw.ElapsedMilliseconds); - sw.Stop(); - } - - private IncidentBeingAnalyzed BuildIncident(ErrorReportEntity entity) - { - if (entity.Exception == null) - return new IncidentBeingAnalyzed(entity); - - if (entity.Exception.Name == "AggregateException") - { - try - { - var exception = entity.Exception; - - //TODO: Check if there are more than one InnerExceptions and then abort this specialization. - while (exception != null && exception.Name == "AggregateException") - { - exception = exception.InnerException; - } - var incident = new IncidentBeingAnalyzed(entity, exception); - return incident; - } - catch (Exception) - { - } - } - - if (entity.Exception.Name == "ReflectionTypeLoadException") - { - try - { - var item = JObject.Parse(entity.Exception.Everything); - var i = new IncidentBeingAnalyzed(entity); - var items = (JObject) item["LoaderExceptions"]; - var exception = items.First; - - - //var incident = new Incident(entity, exception); - //incident.AddIncidentTags(new[] { "ReflectionTypeLoadException" }); - //return incident; - - //TODO: load LoaderExceptions which is an Exception[] array - } - catch (Exception) - { - } - } - - return new IncidentBeingAnalyzed(entity); - } - - private ReportExeptionDTO ConvertToCoreException(ErrorReportException exception) - { - var ex = new ReportExeptionDTO - { - AssemblyName = exception.AssemblyName, - BaseClasses = exception.BaseClasses, - Everything = exception.Everything, - FullName = exception.FullName, - Message = exception.Message, - Name = exception.Name, - Namespace = exception.Namespace, - StackTrace = exception.StackTrace - }; - if (ex.InnerException != null) - ex.InnerException = ConvertToCoreException(exception.InnerException); - return ex; - } - - private ReportDTO ConvertToCoreReport(ErrorReportEntity report) - { - var dto = new ReportDTO - { - ApplicationId = report.ApplicationId, - ContextCollections = - report.ContextInfo.Select(x => new ContextCollectionDTO(x.Name, x.Properties)).ToArray(), - CreatedAtUtc = report.CreatedAtUtc, - Id = report.Id, - IncidentId = report.IncidentId, - RemoteAddress = report.RemoteAddress, - ReportId = report.ClientReportId, - ReportVersion = "1" - }; - if (report.Exception != null) - { - dto.Exception = ConvertToCoreException(report.Exception); - } - return dto; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/app.config b/src/Server/OneTrueError.ReportAnalyzer/app.config deleted file mode 100644 index 936401de..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/app.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.ReportAnalyzer/packages.config b/src/Server/OneTrueError.ReportAnalyzer/packages.config deleted file mode 100644 index 7dcf6631..00000000 --- a/src/Server/OneTrueError.ReportAnalyzer/packages.config +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/ConnectionFactory.cs b/src/Server/OneTrueError.SqlServer.Tests/ConnectionFactory.cs deleted file mode 100644 index 32329340..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/ConnectionFactory.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Configuration; -using System.Data; -using System.Data.SqlClient; -using Griffin.Data; - -namespace OneTrueError.SqlServer.Tests -{ - internal class ConnectionFactory - { - public static IAdoNetUnitOfWork Create() - { - var connection = new SqlConnection(); - connection.ConnectionString = ConfigurationManager.ConnectionStrings["Db"].ConnectionString; - connection.Open(); - return new AdoNetUnitOfWork(connection, true); - } - - public static IDbConnection CreateConnection() - { - var connection = new SqlConnection(); - connection.ConnectionString = ConfigurationManager.ConnectionStrings["Db"].ConnectionString; - connection.Open(); - return connection; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Commands/CreateApiKeyHandlerTests.cs b/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Commands/CreateApiKeyHandlerTests.cs deleted file mode 100644 index b6b72359..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Commands/CreateApiKeyHandlerTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using FluentAssertions; -using Griffin.Data; -using NSubstitute; -using OneTrueError.Api.Core.ApiKeys.Commands; -using OneTrueError.App.Core.Applications; -using OneTrueError.SqlServer.Core.ApiKeys; -using OneTrueError.SqlServer.Core.ApiKeys.Commands; -using OneTrueError.SqlServer.Core.Applications; -using Xunit; - -namespace OneTrueError.SqlServer.Tests.Core.ApiKeys.Commands -{ - public class CreateApiKeyHandlerTests : IDisposable - { - private int _applicationId; - private readonly IAdoNetUnitOfWork _uow; - - public CreateApiKeyHandlerTests() - { - _uow = ConnectionFactory.Create(); - GetApplicationId(); - } - - public void Dispose() - { - _uow.Dispose(); - } - - [Fact] - public async Task should_be_able_to_Create_a_key_without_applications_mapped() - { - var cmd = new CreateApiKey("Mofo", Guid.NewGuid().ToString("N"), Guid.NewGuid().ToString("N")); - var bus = Substitute.For(); - - var sut = new CreateApiKeyHandler(_uow, bus); - await sut.ExecuteAsync(cmd); - - var repos = new ApiKeyRepository(_uow); - var generated = await repos.GetByKeyAsync(cmd.ApiKey); - generated.Should().NotBeNull(); - generated.Claims.Should().BeEmpty(); - } - - [Fact] - public async Task should_be_able_to_Create_key() - { - var cmd = new CreateApiKey("Mofo", Guid.NewGuid().ToString("N"), Guid.NewGuid().ToString("N"), - new[] {_applicationId}); - var bus = Substitute.For(); - - var sut = new CreateApiKeyHandler(_uow, bus); - await sut.ExecuteAsync(cmd); - - var repos = new ApiKeyRepository(_uow); - var generated = await repos.GetByKeyAsync(cmd.ApiKey); - generated.Should().NotBeNull(); - generated.Claims.First().Value.Should().BeEquivalentTo(_applicationId.ToString()); - } - - private void GetApplicationId() - { - var repos = new ApplicationRepository(_uow); - var id = _uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications"); - if (id is DBNull) - { - repos.CreateAsync(new Application(10, "AppTen")).Wait(); - _applicationId = (int) _uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications"); - } - else - _applicationId = (int) id; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Commands/DeleteApiKeyHandlerTests.cs b/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Commands/DeleteApiKeyHandlerTests.cs deleted file mode 100644 index 0d25612b..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Commands/DeleteApiKeyHandlerTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Threading.Tasks; -using FluentAssertions; -using Griffin.Data; -using OneTrueError.Api.Core.ApiKeys.Commands; -using OneTrueError.App.Core.ApiKeys; -using OneTrueError.App.Core.Applications; -using OneTrueError.SqlServer.Core.ApiKeys; -using OneTrueError.SqlServer.Core.ApiKeys.Commands; -using OneTrueError.SqlServer.Core.Applications; -using Xunit; - -namespace OneTrueError.SqlServer.Tests.Core.ApiKeys.Commands -{ - public class DeleteApiKeyHandlerTests : IDisposable - { - private int _applicationId; - private readonly ApiKey _existingEntity; - private readonly IAdoNetUnitOfWork _uow; - - public DeleteApiKeyHandlerTests() - { - _uow = ConnectionFactory.Create(); - GetApplicationId(); - - _existingEntity = new ApiKey - { - ApplicationName = "Arne", - GeneratedKey = Guid.NewGuid().ToString("N"), - SharedSecret = Guid.NewGuid().ToString("N"), - CreatedById = 20, - CreatedAtUtc = DateTime.UtcNow - }; - - _existingEntity.Add(_applicationId); - var repos = new ApiKeyRepository(_uow); - repos.CreateAsync(_existingEntity).Wait(); - } - - public void Dispose() - { - _uow.Dispose(); - } - - [Fact] - public async Task should_be_able_to_delete_key_by_ApiKey() - { - var cmd = new DeleteApiKey(_existingEntity.GeneratedKey); - - var sut = new DeleteApiKeyHandler(_uow); - await sut.ExecuteAsync(cmd); - - var count = _uow.ExecuteScalar("SELECT cast(count(*) as int) FROM ApiKeys WHERE Id = @id", - new {id = _existingEntity.Id}); - count.Should().Be(0); - } - - [Fact] - public async Task should_be_able_to_delete_key_by_id() - { - var cmd = new DeleteApiKey(_existingEntity.Id); - - var sut = new DeleteApiKeyHandler(_uow); - await sut.ExecuteAsync(cmd); - - var count = _uow.ExecuteScalar("SELECT cast(count(*) as int) FROM ApiKeys WHERE Id = @id", - new {id = _existingEntity.Id}); - count.Should().Be(0); - } - - private void GetApplicationId() - { - var repos = new ApplicationRepository(_uow); - var id = _uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications"); - if (id is DBNull) - { - repos.CreateAsync(new Application(10, "AppTen")).Wait(); - _applicationId = (int) _uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications"); - } - else - _applicationId = (int) id; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Queries/GetApiKeyHandlerTests.cs b/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Queries/GetApiKeyHandlerTests.cs deleted file mode 100644 index 83494755..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Queries/GetApiKeyHandlerTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using FluentAssertions; -using Griffin.Data; -using OneTrueError.Api.Core.ApiKeys.Queries; -using OneTrueError.App.Core.ApiKeys; -using OneTrueError.App.Core.Applications; -using OneTrueError.SqlServer.Core.ApiKeys; -using OneTrueError.SqlServer.Core.ApiKeys.Queries; -using OneTrueError.SqlServer.Core.Applications; -using Xunit; - -namespace OneTrueError.SqlServer.Tests.Core.ApiKeys.Queries -{ - public class GetApiKeyHandlerTests - { - private Application _application; - private readonly ApiKey _existingEntity; - private readonly IAdoNetUnitOfWork _uow; - - public GetApiKeyHandlerTests() - { - _uow = ConnectionFactory.Create(); - GetApplication(); - - _existingEntity = new ApiKey - { - ApplicationName = "Arne", - GeneratedKey = Guid.NewGuid().ToString("N"), - SharedSecret = Guid.NewGuid().ToString("N"), - CreatedById = 20, - CreatedAtUtc = DateTime.UtcNow - }; - - _existingEntity.Add(_application.Id); - var repos = new ApiKeyRepository(_uow); - repos.CreateAsync(_existingEntity).Wait(); - } - - public void Dispose() - { - _uow.Dispose(); - } - - - [Fact] - public async void should_Be_able_to_fetch_existing_key_by_id() - { - var query = new GetApiKey(_existingEntity.Id); - - var sut = new GetApiKeyHandler(_uow); - var result = await sut.ExecuteAsync(query); - - result.Should().NotBeNull(); - result.GeneratedKey.Should().Be(_existingEntity.GeneratedKey); - result.ApplicationName.Should().Be(_existingEntity.ApplicationName); - result.AllowedApplications[0].ApplicationId.Should().Be(_application.Id); - result.AllowedApplications[0].ApplicationName.Should().Be(_application.Name); - } - - private void GetApplication() - { - var repos = new ApplicationRepository(_uow); - var id = _uow.ExecuteScalar("SELECT TOP 1 Id FROM Applications"); - if (id is DBNull) - { - _application = new Application(10, "AppTen"); - repos.CreateAsync(_application).Wait(); - } - else - { - _application = repos.GetByIdAsync((int) id).Result; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Queries/ListApiKeysHandlerTests.cs b/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Queries/ListApiKeysHandlerTests.cs deleted file mode 100644 index 79c6fbdf..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/Core/ApiKeys/Queries/ListApiKeysHandlerTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using FluentAssertions; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.ApiKeys.Queries; -using OneTrueError.App.Core.ApiKeys; -using OneTrueError.SqlServer.Core.ApiKeys.Queries; -using Xunit; - -namespace OneTrueError.SqlServer.Tests.Core.ApiKeys.Queries -{ - public class ListApiKeysHandlerTests : IDisposable - { - private readonly ApiKey _existingEntity; - private readonly IAdoNetUnitOfWork _uow; - - public ListApiKeysHandlerTests() - { - _existingEntity = new ApiKey - { - ApplicationName = "Arne", - GeneratedKey = Guid.NewGuid().ToString("N"), - SharedSecret = Guid.NewGuid().ToString("N"), - CreatedById = 20, - CreatedAtUtc = DateTime.UtcNow - }; - _existingEntity.Add(22); - _uow = ConnectionFactory.Create(); - _uow.Insert(_existingEntity); - } - - public void Dispose() - { - _uow.Dispose(); - } - - [Fact] - public async void should_be_able_to_load_a_key() - { - var query = new ListApiKeys(); - - var sut = new ListApiKeysHandler(_uow); - var result = await sut.ExecuteAsync(query); - - result.Keys.Should().NotBeEmpty(); - result.Keys[0].ApiKey.Should().Be(_existingEntity.GeneratedKey); - result.Keys[0].ApplicationName.Should().Be(_existingEntity.ApplicationName); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/Modules/Geolocation/ErrorOriginRepositoryTests.cs b/src/Server/OneTrueError.SqlServer.Tests/Modules/Geolocation/ErrorOriginRepositoryTests.cs deleted file mode 100644 index e031b5a6..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/Modules/Geolocation/ErrorOriginRepositoryTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Threading.Tasks; -using OneTrueError.App.Modules.Geolocation; -using OneTrueError.SqlServer.Modules.Geolocation; -using Xunit; - -namespace OneTrueError.SqlServer.Tests.Modules.Geolocation -{ - public class ErrorOriginRepositoryTests - { - [Fact] - public async Task Can_store_origin() - { - var origin = new ErrorOrigin("127.0.0.1", 934.934, 28.282); - var uow = ConnectionFactory.Create(); - - var handler = new ErrorOriginRepository(uow); - await handler.CreateAsync(origin, 1, 2, 3); - - uow.Dispose(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/OneTrueError.SqlServer.Tests.csproj b/src/Server/OneTrueError.SqlServer.Tests/OneTrueError.SqlServer.Tests.csproj deleted file mode 100644 index cd4b007f..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/OneTrueError.SqlServer.Tests.csproj +++ /dev/null @@ -1,135 +0,0 @@ - - - - - Debug - AnyCPU - {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705} - Library - Properties - OneTrueError.SqlServer.Tests - OneTrueError.SqlServer.Tests - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - ManagedMinimumRules.ruleset - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\FluentAssertions.4.14.0\lib\net45\FluentAssertions.dll - True - - - ..\packages\FluentAssertions.4.14.0\lib\net45\FluentAssertions.Core.dll - True - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.39\lib\net45\Griffin.Core.dll - True - - - ..\packages\log4net.2.0.5\lib\net45-full\log4net.dll - True - - - ..\packages\NSubstitute.1.10.0.0\lib\net45\NSubstitute.dll - True - - - - - - - - - - - - ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll - True - - - ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll - True - - - ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll - True - - - ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll - True - - - - - - - - - - - - - - - - - - - - {FC331A95-FCA4-4764-8004-0884665DD01F} - OneTrueError.Api - - - {5EF42A74-9323-49FA-A1F6-974D6DE77202} - OneTrueError.App - - - {A78A50DA-C9D7-47F2-8528-D7EE39D91924} - OneTrueError.Infrastructure - - - {B967BEEA-CDDD-4A83-A4F2-1C946099ED51} - OneTrueError.SqlServer - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/OneTrueError.SqlServer.Tests.csproj.orig b/src/Server/OneTrueError.SqlServer.Tests/OneTrueError.SqlServer.Tests.csproj.orig deleted file mode 100644 index 1b96ed76..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/OneTrueError.SqlServer.Tests.csproj.orig +++ /dev/null @@ -1,105 +0,0 @@ - - - - - Debug - AnyCPU - {502E6EC1-FF7D-4E1A-846E-D0E8A4EE9705} - Library - Properties - OneTrueError.SqlServer.Tests - OneTrueError.SqlServer.Tests - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.36\lib\net45\Griffin.Core.dll - True - - - ..\packages\log4net.2.0.5\lib\net45-full\log4net.dll - True - - - - - - - - - - - ..\packages\xunit.abstractions.2.0.0\lib\net35\xunit.abstractions.dll - True - - - ..\packages\xunit.assert.2.1.0\lib\dotnet\xunit.assert.dll - True - - - ..\packages\xunit.extensibility.core.2.1.0\lib\dotnet\xunit.core.dll - True - - - ..\packages\xunit.extensibility.execution.2.1.0\lib\net45\xunit.execution.desktop.dll - True - - - - -<<<<<<< HEAD -======= - ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - - - - - - - - - {5EF42A74-9323-49FA-A1F6-974D6DE77202} - OneTrueError.App - - - {B967BEEA-CDDD-4A83-A4F2-1C946099ED51} - OneTrueError.SqlServer - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.SqlServer.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 4f3245dc..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("OneTrueError.SqlServer.Tests")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.SqlServer.Tests")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("502e6ec1-ff7d-4e1a-846e-d0e8a4ee9705")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/SchemaManagerTests.cs b/src/Server/OneTrueError.SqlServer.Tests/SchemaManagerTests.cs deleted file mode 100644 index ba630d4b..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/SchemaManagerTests.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using FluentAssertions; -using Griffin.Data.Mapper; -using Xunit; - -namespace OneTrueError.SqlServer.Tests -{ - public class SchemaManagerTests - { - public const int LatestVersion = 3; - - public SchemaManagerTests() - { - var sut = new SchemaManager(ConnectionFactory.CreateConnection); - EnsureSchemaTable(); - } - - [Fact] - public void should_report_upgradable_if_schema_version_is_less() - { - SetVersion(1); - - var sut = new SchemaManager(ConnectionFactory.CreateConnection); - var actual = sut.CanSchemaBeUpgraded(); - - actual.Should().BeTrue(); - } - - [Fact] - public void should_not_report_upgradable_if_schema_version_is_same() - { - SetVersion(3); - - var sut = new SchemaManager(ConnectionFactory.CreateConnection); - var actual = sut.CanSchemaBeUpgraded(); - - actual.Should().BeFalse(); - } - - [Fact] - public void should_report_upgradable_if_schema_table_is_missing() - { - DropSchemaTable(); - - var sut = new SchemaManager(ConnectionFactory.CreateConnection); - var actual = sut.CanSchemaBeUpgraded(); - - actual.Should().BeTrue(); - } - - [Fact] - public void should_be_able_to_upgrade_schema() - { - - var sut = new SchemaManager(ConnectionFactory.CreateConnection); - sut.UpgradeDatabaseSchema(); - - - } - - - [Fact] - public void should_be_able_to_upgrade_database() - { - using (var tools = new TestTools()) - { - tools.CreateDatabase(); - - var sut = new SchemaManager(tools.CreateConnection); - sut.UpgradeDatabaseSchema(); - } - } - - - private void SetVersion(int version) - { - using (var con = SqlServerTools.OpenConnection()) - { - con.ExecuteNonQuery("UPDATE DatabaseSchema SET Version = @version", new {version = version}); - } - } - - private void EnsureSchemaTable() - { - using (var con = SqlServerTools.OpenConnection()) - { - con.ExecuteNonQuery(@" -IF OBJECT_ID(N'dbo.[DatabaseSchema]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[DatabaseSchema] ( - [Version] int not null default 1 - ); - INSERT INTO DatabaseSchema VALUES(1); -END -"); - } - } - - private void DropSchemaTable() - { - using (var con = SqlServerTools.OpenConnection()) - { - con.ExecuteNonQuery(@"DROP TABLE [dbo].[DatabaseSchema]"); - } - } - } -} diff --git a/src/Server/OneTrueError.SqlServer.Tests/TestTools.cs b/src/Server/OneTrueError.SqlServer.Tests/TestTools.cs deleted file mode 100644 index bb71036c..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/TestTools.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Griffin.Data.Mapper; -using OneTrueError.Infrastructure; - -namespace OneTrueError.SqlServer.Tests -{ - public class TestTools : IDisposable - { - private string _dbName; - - public void CreateDatabase() - { - using (var con = ConnectionFactory.CreateConnection()) - { - _dbName = "T" + Guid.NewGuid().ToString("N"); - con.ExecuteNonQuery("CREATE Database " + _dbName); - con.ChangeDatabase(_dbName); - var schemaTool = new SchemaManager(() => con); - schemaTool.CreateInitialStructure(); - } - } - - public IDbConnection CreateConnection() - { - var connection = ConnectionFactory.CreateConnection(); - connection.ChangeDatabase(_dbName); - return connection; - } - - public void Dispose() - { - if (_dbName == null) - return; - - using (var con = ConnectionFactory.CreateConnection()) - { - var sql = - string.Format("alter database {0} set single_user with rollback immediate; DROP Database {0}", - _dbName); - con.ExecuteNonQuery(sql); - } - } - } -} diff --git a/src/Server/OneTrueError.SqlServer.Tests/app.config b/src/Server/OneTrueError.SqlServer.Tests/app.config deleted file mode 100644 index 96cf3e5b..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/app.config +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer.Tests/packages.config b/src/Server/OneTrueError.SqlServer.Tests/packages.config deleted file mode 100644 index fb7508e7..00000000 --- a/src/Server/OneTrueError.SqlServer.Tests/packages.config +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Analysis/AnalyticsRepository.cs b/src/Server/OneTrueError.SqlServer/Analysis/AnalyticsRepository.cs deleted file mode 100644 index e4f3b5b9..00000000 --- a/src/Server/OneTrueError.SqlServer/Analysis/AnalyticsRepository.cs +++ /dev/null @@ -1,214 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using log4net; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.Infrastructure.Queueing.Msmq; -using OneTrueError.ReportAnalyzer; -using OneTrueError.ReportAnalyzer.Domain.Incidents; -using OneTrueError.ReportAnalyzer.Domain.Reports; -using OneTrueError.ReportAnalyzer.Scanners; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Analysis -{ - [Component] - public class AnalyticsRepository : IAnalyticsRepository, IDisposable - { - private readonly ILog _logger = LogManager.GetLogger(typeof(AnalyticsRepository)); - private readonly ReportDtoConverter _reportDtoConverter = new ReportDtoConverter(); - private readonly IAdoNetUnitOfWork _unitOfWork; - private MsmqMessageQueue _queue; - - public AnalyticsRepository(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - - var settings = ConfigurationStore.Instance.Load(); - if (settings != null && !settings.UseSql) - { - _queue = new MsmqMessageQueue(settings.ReportQueue, settings.ReportAuthentication, - settings.ReportTransactions); - } - } - - public bool ExistsByClientId(string clientReportId) - { - if (clientReportId == null) throw new ArgumentNullException("clientReportId"); - - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = "select id from ErrorReports WHERE ErrorId = @Id"; - cmd.AddParameter("id", clientReportId); - return cmd.ExecuteScalar() != null; - } - } - - public IncidentBeingAnalyzed FindIncidentForReport(int applicationId, string reportHashCode, - string hashCodeIdentifier) - { - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = - @"SELECT Incidents.* - FROM Incidents - WHERE ReportHashCode = @ReportHashCode - AND ApplicationId = @applicationId"; - - - cmd.AddParameter("ReportHashCode", reportHashCode); - cmd.AddParameter("HashCodeIdentifier", hashCodeIdentifier); - cmd.AddParameter("applicationId", applicationId); - - var incidents = cmd.ToList(); - return incidents.FirstOrDefault(incident => incident.HashCodeIdentifier == hashCodeIdentifier); - } - } - - public void CreateIncident(IncidentBeingAnalyzed incident) - { - if (string.IsNullOrEmpty(incident.ReportHashCode)) - throw new InvalidOperationException("ReportHashCode is required to be able to detect duplicates"); - - if (incident == null) throw new ArgumentNullException("incident"); - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = - "INSERT INTO Incidents (ReportHashCode, ApplicationId, CreatedAtUtc, HashCodeIdentifier, ReportCount, UpdatedAtUtc, Description, FullName, IsReOpened)" + - " VALUES (@ReportHashCode, @ApplicationId, @CreatedAtUtc, @HashCodeIdentifier, @ReportCount, @UpdatedAtUtc, @Description, @FullName, 0);select SCOPE_IDENTITY();"; - cmd.AddParameter("Id", incident.Id); - cmd.AddParameter("ReportHashCode", incident.ReportHashCode); - cmd.AddParameter("ApplicationId", incident.ApplicationId); - cmd.AddParameter("CreatedAtUtc", incident.CreatedAtUtc); - cmd.AddParameter("HashCodeIdentifier", incident.HashCodeIdentifier); - cmd.AddParameter("ReportCount", incident.ReportCount); - cmd.AddParameter("UpdatedAtUtc", incident.UpdatedAtUtc); - cmd.AddParameter("Description", incident.Description); - cmd.AddParameter("FullName", incident.FullName); - var id = (int) (decimal) cmd.ExecuteScalar(); - incident.GetType() - .GetProperty("Id", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) - .SetValue(incident, id); - } - //_unitOfWork.Insert(incident); - } - - public void UpdateIncident(IncidentBeingAnalyzed incident) - { - if (incident == null) throw new ArgumentNullException("incident"); - _unitOfWork.Update(incident); - } - - public void CreateReport(ErrorReportEntity report) - { - if (report == null) throw new ArgumentNullException("report"); - - if (string.IsNullOrEmpty(report.Title) && report.Exception != null) - { - report.Title = report.Exception.Message; - if (report.Title.Length > 100) - report.Title = report.Title.Substring(0, 100); - } - - - _unitOfWork.Insert(report); - } - - public string GetAppName(int applicationId) - { - if (applicationId < 1) - throw new ArgumentOutOfRangeException("applicationId", applicationId, "AppId must be a PK"); - - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = "SELECT Name FROM Applications WHERE Id = @id"; - cmd.AddParameter("id", applicationId); - return (string) cmd.ExecuteScalar(); - } - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// 2 - public void Dispose() - { - Dispose(true); - } - - //TODO: Break this out to a separate repository so that we can register it in the container - //using either the SQL or MSMQ implementation. - public IReadOnlyList GetReportsToAnalyze() - { - if (_queue == null) - return GetReportsUsingSql(); - - - var report = _queue.TryReceive(TimeSpan.FromSeconds(1)); - return new[] {report}; - } - - /// - /// Dispose pattern. - /// - /// Invoked from Dispose(). - protected virtual void Dispose(bool isDisposing) - { - if (_queue != null) - { - _queue.Dispose(); - _queue = null; - } - } - - private IReadOnlyList GetReportsUsingSql() - { - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = @"SELECT QueueReports.* - FROM QueueReports - ORDER BY QueueReports.Id"; - cmd.Limit(10); - - try - { - var reports = new List(); - var idsToRemove = new List(); - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var json = ""; - try - { - json = (string) reader["body"]; - var report = _reportDtoConverter.LoadReportFromJson(json); - var newReport = _reportDtoConverter.ConvertReport(report, (int) reader["ApplicationId"]); - newReport.RemoteAddress = (string) reader["RemoteAddress"]; - reports.Add(newReport); - idsToRemove.Add(reader.GetInt32(0)); - } - catch (Exception ex) - { - _logger.Error("Failed to deserialize " + json, ex); - } - } - } - if (idsToRemove.Any()) - _unitOfWork.ExecuteNonQuery("DELETE FROM QueueReports WHERE Id IN (" + - string.Join(",", idsToRemove) + ")"); - return reports; - } - catch (Exception ex) - { - throw cmd.CreateDataException(ex); - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Analysis/ErrorReportEntityMapper.cs b/src/Server/OneTrueError.SqlServer/Analysis/ErrorReportEntityMapper.cs deleted file mode 100644 index ec520eb4..00000000 --- a/src/Server/OneTrueError.SqlServer/Analysis/ErrorReportEntityMapper.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.ReportAnalyzer.Domain.Reports; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Analysis -{ - public class ErrorReportEntityMapper : CrudEntityMapper - { - public ErrorReportEntityMapper() : base("ErrorReports") - { - Property(x => x.Id) - .PrimaryKey(true); - - Property(x => x.Exception) - .ToPropertyValue(EntitySerializer.Deserialize) - .ToColumnValue(EntitySerializer.Serialize); - - Property(x => x.ContextInfo) - .ToPropertyValue(EntitySerializer.Deserialize) - .ToColumnValue(EntitySerializer.Serialize); - - Property(x => x.ClientReportId) - .ColumnName("ErrorId"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Analysis/ErrorReportRepository.cs b/src/Server/OneTrueError.SqlServer/Analysis/ErrorReportRepository.cs deleted file mode 100644 index 0d604c61..00000000 --- a/src/Server/OneTrueError.SqlServer/Analysis/ErrorReportRepository.cs +++ /dev/null @@ -1,185 +0,0 @@ -//using System; -//using System.Collections.Generic; -//using System.Data.Common; -//using System.Linq; -//using System.Threading.Tasks; -//using Griffin.Container; -//using Griffin.Data; -//using Griffin.Data.Mapper; -//using OneTrueError.Core; -//using OneTrueError.ReportsAnalytics.Reports; -//using OneTrueError.UnitOfWork; - -//namespace OneTrueError.ReportAnalytics.Data.SqlServer -//{ - -// // public interface IReportsRepository -// // { -// // Task Create(InvalidErrorReport invalidReport); -// // /// -// // /// Customer specific id. -// // /// -// // /// -// // /// -// // //Task FindByErrorIdAsync(string errorId); -// // Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize); - -// // /// -// // /// Finds the by error identifier asynchronous. -// // /// -// // /// Customer generated id (from the client library). -// // /// -// // Task FindByErrorIdAsync(string errorId); -// // } - - -// [Component] -// internal class ErrorReportRepository// : IReportsRepository -// { -// private IAdoNetUnitOfWork _uow; - -// public ErrorReportRepository(CustomerUnitOfWork uow) -// { -// if (uow == null) throw new ArgumentNullException("uow"); - -// _uow = uow; -// } - -// public void Create(ErrorReportEntity entity) -// { -// using (var cmd = _uow.CreateCommand()) -// { -// cmd.CommandText = "INSERT INTO ErrorReports (Id, ErrorId, ApplicationId, Title, Exception, ReportHashCode, HashCodeIdentifier, IncidentId, CreatedAtUtc, ContextInfo)" -// + -// " VALUES (@Id, @ErrorId, @ApplicationId, @Title, @Exception, @ReportHashCode, @HashCodeIdentifier, @IncidentId, @CreatedAtUtc, @ContextInfo)"; - -// var ex = JsonSerializer.Serialize(entity.Exception); -// var contexts = JsonSerializer.Serialize(entity.ContextInfo); -// cmd.AddParameter("Id", entity.Id); -// cmd.AddParameter("ErrorId", entity.ClientReportId); -// cmd.AddParameter("ApplicationId", entity.ApplicationId); -// cmd.AddParameter("Exception", ex); -// cmd.AddParameter("ReportHashCode", entity.ReportHashCode); -// cmd.AddParameter("HashCodeIdentifier", entity.HashCodeIdentifier); -// cmd.AddParameter("IncidentId", entity.IncidentId); -// cmd.AddParameter("CreatedAtUtc", entity.CreatedAtUtc); -// cmd.AddParameter("ContextInfo", contexts); - -// if (entity.Exception != null) -// { -// var pos = entity.Exception.Message.IndexOfAny(new[] { '\r', '\n' }); -// cmd.AddParameter("Title", pos == -1 -// ? entity.Exception.Message -// : entity.Exception.Message.Substring(0, pos)); -// } -// else -// cmd.AddParameter("Title", ""); - -// cmd.ExecuteNonQuery(); -// } -// } - - -// public void Update(ErrorReportEntity entity) -// { -// var ex = JsonSerializer.Serialize(entity.Exception); -// var contexts = JsonSerializer.Serialize(entity.ContextInfo); - - -// using (var cmd = _uow.CreateCommand()) -// { -// cmd.CommandText = @"UPDATE ErrorReports SET ErrorId = @ErrorId, -//ApplicationId = @ApplicationId, -//Exception = @Exception, -//ReportHashCode = @ReportHashCode, -//HashCodeIdentifier = @HashCodeIdentifier, -//IncidentId = @incidentId, -//CreatedAtUtc = @CreatedAtUtc, -//ContextInfo = @ContextInfo -//WHERE Id = @id"; - -// cmd.AddParameter("ErrorId", entity.ClientReportId); -// cmd.AddParameter("ApplicationId", entity.ApplicationId); -// cmd.AddParameter("Exception", ex); -// cmd.AddParameter("ReportHashCode", entity.ReportHashCode); -// cmd.AddParameter("HashCodeIdentifier", entity.HashCodeIdentifier); -// cmd.AddParameter("IncidentId", entity.IncidentId); -// cmd.AddParameter("CreatedAtUtc", entity.CreatedAtUtc); -// cmd.AddParameter("ContextInfo", contexts); -// cmd.AddParameter("Id", entity.Id); -// cmd.ExecuteNonQuery(); -// } -// } - - -// //public async Task Create(InvalidErrorReport entity) -// //{ -// // using (var cmd = (DbCommand)_uow.CreateCommand()) -// // { -// // cmd.CommandText = -// // @"INSERT INTO InvalidErrorReports (Id, AddedAtUtc, ApplicationId, Body, Exception) VALUES(@Id, @AddedAtUtc, @OrganizationId, @ApplicationId, @Body, @Exception)"; -// // cmd.AddParameter("Id", entity.Id); -// // cmd.AddParameter("AddedAtUtc", entity.AddedAtUtc); -// // cmd.AddParameter("ApplicationId", entity.ApplicationId); -// // cmd.AddParameter("Body", entity.Report); -// // cmd.AddParameter("Exception", entity.Exception); -// // await cmd.ExecuteNonQueryAsync(); -// // } -// //} - - -// //public async Task FindByErrorIdAsync(string errorId) -// //{ -// // using (var cmd = (DbCommand)_uow.CreateCommand()) -// // { -// // cmd.CommandText = -// // "SELECT * FROM ErrorReports WHERE ErrorId = @id"; - -// // cmd.AddParameter("id", errorId); -// // return await cmd.FirstOrDefaultAsync(); -// // } -// //} - -// //public async Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize) -// //{ -// // using (var cmd = (DbCommand)_uow.CreateCommand()) -// // { -// // cmd.AddParameter("incidentId", incidentId); -// // long totalRows = 0; -// // if (pageNumber > 0) -// // { -// // cmd.CommandText = -// //"SELECT count(*) FROM ErrorReports WHERE IncidentId = @incidentId"; -// // totalRows = (int)await cmd.ExecuteScalarAsync(); -// // } - -// // cmd.CommandText = -// // "SELECT * FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY CreatedAtUtc DESC"; -// // if (pageNumber > 0) -// // { -// // var offset = (pageNumber - 1)*pageSize; -// // cmd.CommandText += string.Format(@" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize); -// // } - -// // //cmd.AddParameter("incidentId", incidentId); -// // var list = await cmd.ToListAsync(); -// // return new PagedReports() -// // { -// // TotalCount = (int) totalRows, -// // Reports = (IReadOnlyList)list -// // }; -// // } -// //} - -// public async Task> GetForIncidentAsync(int incidentId) -// { -// using (var cmd = (DbCommand)_uow.CreateCommand()) -// { -// cmd.CommandText = "SELECT * FROM ErrorReports WHERE IncidentId = @id"; -// cmd.AddParameter("id", incidentId); -// return await cmd.ToListAsync(); -// } -// } -// } -//} - diff --git a/src/Server/OneTrueError.SqlServer/Analysis/Incident2Mapper.cs b/src/Server/OneTrueError.SqlServer/Analysis/Incident2Mapper.cs deleted file mode 100644 index a5df3adb..00000000 --- a/src/Server/OneTrueError.SqlServer/Analysis/Incident2Mapper.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using Griffin.Data.Mapper; -using OneTrueError.ReportAnalyzer.Domain.Incidents; - -namespace OneTrueError.SqlServer.Analysis -{ - public class Incident2Mapper : CrudEntityMapper - { - public Incident2Mapper() - : base("Incidents") - { - Property(x => x.UpdatedAtUtc) - .ToPropertyValue(DbConverters.ToEntityDate) - .ToColumnValue(DbConverters.ToNullableSqlDate); - - Property(x => x.PreviousSolutionAtUtc) - .ToPropertyValue(DbConverters.ToEntityDate) - .ToColumnValue(DbConverters.ToNullableSqlDate); - - Property(x => x.ReOpenedAtUtc) - .ToPropertyValue(DbConverters.ToEntityDate) - .ToColumnValue(DbConverters.ToNullableSqlDate); - - Property(x => x.SolvedAtUtc) - .ToPropertyValue(DbConverters.ToEntityDate) - .ToColumnValue(DbConverters.ToNullableSqlDate); - - Property(x => x.IsSolved) - .ToPropertyValue(o => Convert.ToBoolean(((byte[]) o)[0])); - - Property(x => x.IsIgnored) - .ColumnName("IgnoreReports"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Analysis/ScanForNewFeedback.cs b/src/Server/OneTrueError.SqlServer/Analysis/ScanForNewFeedback.cs deleted file mode 100644 index 2083c917..00000000 --- a/src/Server/OneTrueError.SqlServer/Analysis/ScanForNewFeedback.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using DotNetCqs; -using Griffin.ApplicationServices; -using Griffin.Container; -using log4net; -using Newtonsoft.Json; -using OneTrueError.Api.Core.Feedback.Commands; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.ReportAnalyzer.LibContracts; - -namespace OneTrueError.SqlServer.Analysis -{ - /// - /// TODO: In a perfect world, the BL should be moved to BL and the data should remain in the data. But let's not - /// concern the separation. - /// - [Component(Lifetime = Lifetime.Singleton)] - public class ScanForNewFeedback : ApplicationServiceTimer - { - private readonly ICommandBus _cmdBus; - private readonly ILog _logger = LogManager.GetLogger(typeof(ScanForNewFeedback)); - private readonly IMessageQueue _messageQueue; - - public ScanForNewFeedback(ICommandBus cmdBus, IMessageQueueProvider queueProvider) - { - _cmdBus = cmdBus; - _messageQueue = queueProvider.Open("FeedbackQueue"); - Interval = TimeSpan.FromSeconds(1); - } - - - protected override void Execute() - { - while (true) - { - var dto = _messageQueue.Receive(); - if (dto == null) - break; - - try - { - var submitCmd = new SubmitFeedback(dto.ReportId, dto.RemoteAddress) - { - CreatedAtUtc = dto.ReceivedAtUtc, - Email = dto.EmailAddress, - Feedback = dto.Description - }; - _cmdBus.ExecuteAsync(submitCmd).Wait(); - } - catch (Exception ex) - { - _logger.Error("Failed to process " + JsonConvert.SerializeObject(dto), ex); - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Accounts/AccountRepository.cs b/src/Server/OneTrueError.SqlServer/Core/Accounts/AccountRepository.cs deleted file mode 100644 index 73055d90..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Accounts/AccountRepository.cs +++ /dev/null @@ -1,188 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Linq; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.App.Core.Accounts; - -namespace OneTrueError.SqlServer.Core.Accounts -{ - [Component] - public class AccountRepository : IAccountRepository - { - private readonly IAdoNetUnitOfWork _uow; - - public AccountRepository(IAdoNetUnitOfWork uow) - { - if (uow == null) throw new ArgumentNullException("uow"); - _uow = uow; - } - - /* - public string Id { get; private set; } - public string UserName { get; private set; } - public string HashedPassword { get; private set; } - public string Salt { get; private set; } - public DateTime CreatedAtUtc { get; private set; } - public AccountState AccountState { get; set; } - public string Email { get; private set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public DateTime UpdatedAtUtc { get; set; } - public string CompanyName { get; set; } - public string LastUsedOrganization { get; set; } - public string ActivationKey { get; set; } - public int LoginAttempts { get; private set; } - public DateTime LastLoginAtUtc { get; private set; }*/ - - public async Task CreateAsync(Account account) - { - await _uow.InsertAsync(account); - } - - public async Task FindByActivationKeyAsync(string activationKey) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = "SELECT * FROM Accounts WHERE ActivationKey=@key"; - cmd.AddParameter("key", activationKey); - return await cmd.FirstOrDefaultAsync(new AccountMapper()); - } - } - - public async Task UpdateAsync(Account account) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = - "UPDATE Accounts SET " + - " Username = @Username, " + - " HashedPassword = @HashedPassword, " + - " Salt = @Salt, " + - " CreatedAtUtc = @CreatedAtUtc, " + - " AccountState = @AccountState, " + - " Email = @Email, " + - " UpdatedAtUtc = @UpdatedAtUtc, " + - " ActivationKey = @ActivationKey, " + - " LoginAttempts = @LoginAttempts, " + - " LastLoginAtUtc = @LastLoginAtUtc " + - "WHERE Id = @Id"; - cmd.AddParameter("@Id", account.Id); - cmd.AddParameter("@Username", account.UserName); - cmd.AddParameter("@HashedPassword", account.HashedPassword); - cmd.AddParameter("@Salt", account.Salt); - cmd.AddParameter("@CreatedAtUtc", account.CreatedAtUtc); - cmd.AddParameter("@AccountState", account.AccountState.ToString()); - cmd.AddParameter("@Email", account.Email); - cmd.AddParameter("@UpdatedAtUtc", - account.UpdatedAtUtc == DateTime.MinValue ? (object) null : account.UpdatedAtUtc); - cmd.AddParameter("@ActivationKey", account.ActivationKey); - cmd.AddParameter("@LoginAttempts", account.LoginAttempts); - cmd.AddParameter("@LastLoginAtUtc", - account.LastLoginAtUtc == DateTime.MinValue ? (object) null : account.LastLoginAtUtc); - await cmd.ExecuteNonQueryAsync(); - } - } - - public async Task FindByUserNameAsync(string userName) - { - if (userName == null) throw new ArgumentNullException("userName"); - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = "SELECT TOP 1 * FROM Accounts WHERE UserName=@uname"; - cmd.AddParameter("uname", userName); - return await cmd.FirstOrDefaultAsync(new AccountMapper()); - } - } - - public async Task GetByIdAsync(int id) - { - if (id <= 0) throw new ArgumentNullException("id"); - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = "SELECT * FROM Accounts WHERE Id=@id"; - cmd.AddParameter("id", id); - return await cmd.FirstAsync(new AccountMapper()); - } - } - - public async Task FindByEmailAsync(string emailAddress) - { - if (emailAddress == null) throw new ArgumentNullException("emailAddress"); - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = "SELECT * FROM Accounts WHERE Email=@email"; - cmd.AddParameter("email", emailAddress); - return await cmd.FirstOrDefaultAsync(new AccountMapper()); - } - } - - public async Task> GetByIdAsync(int[] ids) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = "SELECT * FROM Accounts WHERE Id IN (@ids)"; - cmd.AddParameter("ids", string.Join(",", ids.Select(x => "'" + x + "'"))); - return await cmd.ToListAsync(); - } - } - - - public async Task IsEmailAddressTakenAsync(string email) - { - if (email == null) throw new ArgumentNullException("email"); - using (var cmd = _uow.CreateDbCommand()) - { - cmd.CommandText = "SELECT TOP 1 Email FROM Accounts WHERE Email = @Email"; - cmd.AddParameter("Email", email); - var result = await cmd.ExecuteScalarAsync(); - return result != null && result != DBNull.Value; - } - } - - - public async Task IsUserNameTakenAsync(string userName) - { - if (userName == null) throw new ArgumentNullException("userName"); - using (var cmd = _uow.CreateDbCommand()) - { - cmd.CommandText = "SELECT TOP 1 UserName FROM Accounts WHERE UserName = @userName"; - cmd.AddParameter("userName", userName); - var result = await cmd.ExecuteScalarAsync(); - return result != null && result != DBNull.Value; - } - } - - public void Create(Account account) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = - "INSERT INTO Accounts (Id, Username, HashedPassword, Salt, CreatedAtUtc, AccountState, Email, UpdatedAtUtc, ActivationKey, LoginAttempts, LastLoginAtUtc) " + - " VALUES(@Id, @Username, @HashedPassword, @Salt, @CreatedAtUtc, @AccountState, @Email, @UpdatedAtUtc, @ActivationKey, @LoginAttempts, @LastLoginAtUtc)"; - cmd.AddParameter("@Id", account.Id); - cmd.AddParameter("@Username", account.UserName); - cmd.AddParameter("@HashedPassword", account.HashedPassword); - cmd.AddParameter("@Salt", account.Salt); - cmd.AddParameter("@CreatedAtUtc", account.CreatedAtUtc); - cmd.AddParameter("@AccountState", account.AccountState.ToString()); - cmd.AddParameter("@Email", account.Email); - cmd.AddParameter("@UpdatedAtUtc", - account.UpdatedAtUtc == DateTime.MinValue ? (object) null : account.UpdatedAtUtc); - cmd.AddParameter("@ActivationKey", account.ActivationKey); - cmd.AddParameter("@LoginAttempts", account.LoginAttempts); - cmd.AddParameter("@LastLoginAtUtc", - account.LastLoginAtUtc == DateTime.MinValue ? (object) null : account.LastLoginAtUtc); - cmd.ExecuteNonQuery(); - } - } - - public Account GetByUserName(string userName) - { - return _uow.First(new {UserName = userName}); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs b/src/Server/OneTrueError.SqlServer/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs deleted file mode 100644 index cea11db3..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Accounts/QueryHandlers/GetAccountEmailByIdHandler.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using OneTrueError.Api.Core.Accounts.Queries; -using OneTrueError.App.Core.Accounts; - -namespace OneTrueError.SqlServer.Core.Accounts.QueryHandlers -{ - [Component] - public class GetAccountEmailByIdHandler : IQueryHandler - { - private readonly IAccountRepository _accountRepository; - - public GetAccountEmailByIdHandler(IAccountRepository accountRepository) - { - if (accountRepository == null) throw new ArgumentNullException(nameof(accountRepository)); - _accountRepository = accountRepository; - } - - public async Task ExecuteAsync(GetAccountEmailById query) - { - var usr = await _accountRepository.GetByIdAsync(query.AccountId); - return usr.Email; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/ApiKeyMapper.cs b/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/ApiKeyMapper.cs deleted file mode 100644 index 39918e41..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/ApiKeyMapper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.App.Core.ApiKeys; - -namespace OneTrueError.SqlServer.Core.ApiKeys.Mappings -{ - public class ApiKeyMapper : CrudEntityMapper - { - public ApiKeyMapper() : base("ApiKeys") - { - Property(x => x.Id) - .PrimaryKey(true); - Property(x => x.Claims) - .Ignore(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/MirrorMapper.cs b/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/MirrorMapper.cs deleted file mode 100644 index f8ef5428..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Mappings/MirrorMapper.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Data; -using System.Reflection; -using Griffin.Data.Mapper; - -namespace OneTrueError.SqlServer.Core.ApiKeys.Mappings -{ - public class MirrorMapper : IEntityMapper where T : new() - { - private MethodInfo[] _setters; - - public void Map(IDataRecord source, T destination) - { - Map(source, (object) destination); - } - - public object Create(IDataRecord record) - { - return new T(); - } - - public void Map(IDataRecord source, object destination) - { - if (_setters == null) - _setters = MapPropertySetters(source, typeof(T)); - - for (var i = 0; i < _setters.Length; i++) - { - var value = source[i]; - if (value is DBNull) - continue; - - _setters[i].Invoke(destination, new[] {value}); - } - } - - private MethodInfo[] MapPropertySetters(IDataRecord source, Type type) - { - var fields = new MethodInfo[source.FieldCount]; - for (var i = 0; i < source.FieldCount; i++) - { - var name = source.GetName(i); - fields[i] = - type.GetProperty(name, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) - .SetMethod; - } - return fields; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Queries/ListApiKeysHandler.cs b/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Queries/ListApiKeysHandler.cs deleted file mode 100644 index a5dd8fde..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/ApiKeys/Queries/ListApiKeysHandler.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.ApiKeys.Queries; -using OneTrueError.SqlServer.Core.ApiKeys.Mappings; - -namespace OneTrueError.SqlServer.Core.ApiKeys.Queries -{ - [Component(RegisterAsSelf = true)] - public class ListApiKeysHandler : IQueryHandler - { - private readonly MirrorMapper _mapper = new MirrorMapper(); - private readonly IAdoNetUnitOfWork _unitOfWork; - - public ListApiKeysHandler(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task ExecuteAsync(ListApiKeys query) - { - var keys = - await - _unitOfWork.ToListAsync(_mapper, - "SELECT ID, GeneratedKey ApiKey, ApplicationName FROM ApiKeys ORDER BY ApplicationName"); - return new ListApiKeysResult {Keys = keys.ToArray()}; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Applications/ApplicationMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Applications/ApplicationMapper.cs deleted file mode 100644 index d1857e79..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Applications/ApplicationMapper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Applications; -using OneTrueError.App.Core.Applications; - -namespace OneTrueError.SqlServer.Core.Applications -{ - public class ApplicationMapper : CrudEntityMapper - { - public ApplicationMapper() : base("Applications") - { - Property(x => x.ApplicationType) - .ToPropertyValue(o => (TypeOfApplication) Enum.Parse(typeof(TypeOfApplication), (string) o)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Applications/ApplicationRepository.cs b/src/Server/OneTrueError.SqlServer/Core/Applications/ApplicationRepository.cs deleted file mode 100644 index a167f58c..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Applications/ApplicationRepository.cs +++ /dev/null @@ -1,189 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Linq; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.App.Core.Applications; -using OneTrueError.App.Core.Users; - -namespace OneTrueError.SqlServer.Core.Applications -{ - [Component] - public class ApplicationRepository : IApplicationRepository - { - private readonly IAdoNetUnitOfWork _uow; - - public ApplicationRepository(IAdoNetUnitOfWork uow) - { - if (uow == null) throw new ArgumentNullException("uow"); - _uow = uow; - } - - public async Task CreateAsync(ApplicationTeamMember member) - { - await _uow.InsertAsync(member); - } - - public async Task GetForUserAsync(int accountId) - { - if (accountId <= 0) throw new ArgumentOutOfRangeException(nameof(accountId)); - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = @"SELECT a.Id ApplicationId, a.Name ApplicationName, ApplicationMembers.Roles - FROM Applications a - JOIN ApplicationMembers ON (ApplicationMembers.ApplicationId = a.Id) - WHERE ApplicationMembers.AccountId = @userId"; - cmd.AddParameter("userId", accountId); - using (var reader = await cmd.ExecuteReaderAsync()) - { - List apps = new List(); - while (await reader.ReadAsync()) - { - var a = new UserApplication - { - IsAdmin = reader.GetString(2).Contains("Admin"), - ApplicationName = reader.GetString(1), - ApplicationId = reader.GetInt32(0) - }; - apps.Add(a); - } - return apps.ToArray(); - } - } - } - - public async Task UpdateAsync(ApplicationTeamMember member) - { - await _uow.UpdateAsync(member); - } - - public async Task> GetTeamMembersAsync(int applicationId) - { - return await _uow.ToListAsync(@"SELECT Users.UserName, ApplicationMembers.* - FROM ApplicationMembers - LEFT JOIN Users ON (Users.AccountId = ApplicationMembers.AccountId) - WHERE ApplicationId = @1", applicationId); - } - - - public async Task GetByKeyAsync(string appKey) - { - if (appKey == null) throw new ArgumentNullException("appKey"); - - using (var cmd = _uow.CreateDbCommand()) - { - cmd.CommandText = - "SELECT * FROM Applications WHERE AppKey = @id"; - - cmd.AddParameter("id", appKey); - var item = await cmd.FirstOrDefaultAsync(new ApplicationMapper()); - if (item == null) - throw new EntityNotFoundException(appKey, cmd); - return item; - } - } - - /*Id uniqueidentifier not null primary key, - Title nvarchar(50) not null, - AppKey uniqueidentifier not null, - OrganizationId uniqueidentifier not null, - CreatedById uniqueidentifier not null, - CreatedAtUtc datetime2 not null, - ApplicationType varchar(40) not null, - SharedSecret uniqueidentifier not null,*/ - - public async Task GetByIdAsync(int id) - { - if (id == 0) - throw new ArgumentNullException("id"); - - using (var cmd = _uow.CreateDbCommand()) - { - cmd.CommandText = - "SELECT * FROM Applications WHERE Id = @id"; - - cmd.AddParameter("id", id); - var item = await cmd.FirstOrDefaultAsync(); - if (item == null) - throw new EntityNotFoundException(id.ToString(), cmd); - - return item; - } - } - - public async Task CreateAsync(Application application) - { - if (application == null) throw new ArgumentNullException("application"); - - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = - @"INSERT INTO Applications (Name, AppKey, CreatedById, CreatedAtUtc, ApplicationType, SharedSecret) - VALUES(@Name, @AppKey, @CreatedById, @CreatedAtUtc, @ApplicationType, @SharedSecret);SELECT SCOPE_IDENTITY();"; - cmd.AddParameter("Name", application.Name); - cmd.AddParameter("AppKey", application.AppKey); - cmd.AddParameter("CreatedById", application.CreatedById); - cmd.AddParameter("CreatedAtUtc", application.CreatedAtUtc); - cmd.AddParameter("ApplicationType", application.ApplicationType.ToString()); - cmd.AddParameter("SharedSecret", application.SharedSecret); - var item = (decimal) await cmd.ExecuteScalarAsync(); - application.GetType().GetProperty("Id").SetValue(application, (int) item); - } - } - - - public async Task DeleteAsync(int applicationId) - { - if (applicationId == 0) throw new ArgumentNullException("applicationId"); - - using (var cmd = _uow.CreateDbCommand()) - { - cmd.CommandText = - "DELETE FROM Applications WHERE Id = @id"; - - //TODO: Delete reports?? - // or save them for future analysis? - - cmd.AddParameter("id", applicationId); - await cmd.ExecuteNonQueryAsync(); - } - } - - public async Task GetAllAsync() - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = "SELECT * FROM Applications"; - - //cmd.AddParameter("ids", string.Join(", ", appIds.Select(x => "'" + x + "'"))); - var result = await cmd.ToListAsync(); - return result.ToArray(); - } - } - - public async Task DeleteAsync(Application application) - { - if (application == null) throw new ArgumentNullException("application"); - await DeleteAsync(application.Id); - } - - public async Task UpdateAsync(Application entity) - { - await _uow.UpdateAsync(entity); - } - - public async Task RemoveTeamMemberAsync(int applicationId, int userId) - { - using (var cmd = (DbCommand)_uow.CreateCommand()) - { - cmd.CommandText = "DELETE FROM ApplicationMembers WHERE ApplicationId=@appId AND AccountId = @userId"; - cmd.AddParameter("appId", applicationId); - cmd.AddParameter("userId", userId); - await cmd.ExecuteNonQueryAsync(); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs b/src/Server/OneTrueError.SqlServer/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs deleted file mode 100644 index 7b8437de..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Applications/Queries/GetApplicationIdByKeyHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using OneTrueError.Api.Core.Applications.Queries; - -namespace OneTrueError.SqlServer.Core.Applications.Queries -{ - [Component] - public class GetApplicationIdByKeyHandler : IQueryHandler - { - private readonly IAdoNetUnitOfWork _uow; - - public GetApplicationIdByKeyHandler(IAdoNetUnitOfWork uow) - { - _uow = uow; - } - - public async Task ExecuteAsync(GetApplicationIdByKey query) - { - using (var cmd = _uow.CreateDbCommand()) - { - cmd.CommandText = "SELECT Id FROM Applications WHERE AppKey = @appKey"; - cmd.AddParameter("appKey", query.ApplicationKey); - return new GetApplicationIdByKeyResult {Id = (int) await cmd.ExecuteScalarAsync()}; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Applications/Queries/GetApplicationOverviewHandler.cs b/src/Server/OneTrueError.SqlServer/Core/Applications/Queries/GetApplicationOverviewHandler.cs deleted file mode 100644 index 5e841b8a..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Applications/Queries/GetApplicationOverviewHandler.cs +++ /dev/null @@ -1,220 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using OneTrueError.Api.Core.Applications.Queries; - -namespace OneTrueError.SqlServer.Core.Applications.Queries -{ - [Component] - internal class GetApplicationOverviewHandler : IQueryHandler - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public GetApplicationOverviewHandler(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task ExecuteAsync(GetApplicationOverview query) - { - if (query.NumberOfDays == 0) - query.NumberOfDays = 30; - - if (query.NumberOfDays == 1) - return await GetTodaysOverviewAsync(query); - - var curDate = DateTime.Today.AddDays(-query.NumberOfDays); - var errorReports = new Dictionary(); - var incidents = new Dictionary(); - while (curDate <= DateTime.Today) - { - errorReports[curDate] = 0; - incidents[curDate] = 0; - curDate = curDate.AddDays(1); - } - - var result = new GetApplicationOverviewResult(); - using (var cmd = _unitOfWork.CreateDbCommand()) - { - var sql = @"select cast(Incidents.CreatedAtUtc as date), count(Id) -from Incidents -where Incidents.CreatedAtUtc >= @minDate -AND Incidents.CreatedAtUtc <= GetUtcDate() -{0} -group by cast(Incidents.CreatedAtUtc as date); -select cast(ErrorReports.CreatedAtUtc as date), count(Id) -from ErrorReports -where ErrorReports.CreatedAtUtc >= @minDate -AND ErrorReports.CreatedAtUtc <= GetUtcDate() -{1} -group by cast(ErrorReports.CreatedAtUtc as date);"; - - if (query.ApplicationId > 0) - { - cmd.CommandText = string.Format(sql, - " AND Incidents.ApplicationId = @appId", - " AND ErrorReports.ApplicationId = @appId"); - cmd.AddParameter("appId", query.ApplicationId); - } - else - { - cmd.CommandText = string.Format(sql, "", ""); - } - - cmd.AddParameter("minDate", DateTime.Today.AddDays(-query.NumberOfDays)); - using (var reader = await cmd.ExecuteReaderAsync()) - { - while (await reader.ReadAsync()) - { - incidents[(DateTime) reader[0]] = (int) reader[1]; - } - await reader.NextResultAsync(); - while (await reader.ReadAsync()) - { - errorReports[(DateTime) reader[0]] = (int) reader[1]; - } - - result.ErrorReports = errorReports.Select(x => x.Value).ToArray(); - result.Incidents = incidents.Select(x => x.Value).ToArray(); - result.TimeAxisLabels = incidents.Select(x => x.Key.ToShortDateString()).ToArray(); - } - } - - await GetStatSummary(query, result); - - - return result; - } - - private async Task GetStatSummary(GetApplicationOverview query, GetApplicationOverviewResult result) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = @"select count(id) from incidents -where CreatedAtUtc >= @minDate -AND CreatedAtUtc <= GetUtcDate() -AND ApplicationId = @appId -AND Incidents.IgnoreReports = 0 -AND Incidents.IsSolved = 0; - -select count(id) from errorreports -where CreatedAtUtc >= @minDate -AND CreatedAtUtc <= GetUtcDate() -AND ApplicationId = @appId; - -select count(distinct emailaddress) from IncidentFeedback -where CreatedAtUtc >= @minDate -AND CreatedAtUtc <= GetUtcDate() -AND ApplicationId = @appId -AND emailaddress is not null -AND DATALENGTH(emailaddress) > 0; - -select count(*) from IncidentFeedback -where CreatedAtUtc >= @minDate -AND CreatedAtUtc <= GetUtcDate() -AND ApplicationId = @appId -AND Description is not null -AND DATALENGTH(Description) > 0;"; - cmd.AddParameter("appId", query.ApplicationId); - var minDate = query.NumberOfDays == 1 - ? DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23) - : DateTime.Today.AddDays(-query.NumberOfDays); - cmd.AddParameter("minDate", minDate); - - using (var reader = await cmd.ExecuteReaderAsync()) - { - if (!await reader.ReadAsync()) - { - throw new InvalidOperationException("Expected to be able to read."); - } - - var data = new OverviewStatSummary(); - data.Incidents = reader.GetInt32(0); - await reader.NextResultAsync(); - await reader.ReadAsync(); - data.Reports = reader.GetInt32(0); - await reader.NextResultAsync(); - await reader.ReadAsync(); - data.Followers = reader.GetInt32(0); - await reader.NextResultAsync(); - await reader.ReadAsync(); - data.UserFeedback = reader.GetInt32(0); - result.StatSummary = data; - } - } - } - - private async Task GetTodaysOverviewAsync(GetApplicationOverview query) - { - var result = new GetApplicationOverviewResult - { - TimeAxisLabels = new string[24] - }; - var incidentValues = new Dictionary(); - var reportValues = new Dictionary(); - - var startDate = DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-23); - for (var i = 0; i < 24; i++) - { - result.TimeAxisLabels[i] = startDate.AddHours(i).ToString("HH:mm"); - incidentValues[startDate.AddHours(i)] = 0; - reportValues[startDate.AddHours(i)] = 0; - } - - using (var cmd = _unitOfWork.CreateDbCommand()) - { - var sql = @"SELECT DATEPART(HOUR, Incidents.CreatedAtUtc), cast(count(Id) as int) -from Incidents -where Incidents.CreatedAtUtc >= @minDate -AND Incidents.CreatedAtUtc <= GetUtcDate() -{0} -group by DATEPART(HOUR, Incidents.CreatedAtUtc); -select DATEPART(HOUR, ErrorReports.CreatedAtUtc), cast(count(Id) as int) -from ErrorReports -where ErrorReports.CreatedAtUtc >= @minDate -AND ErrorReports.CreatedAtUtc <= GetUtcDate() -{1} -group by DATEPART(HOUR, ErrorReports.CreatedAtUtc);"; - - cmd.CommandText = string.Format(sql, - " AND Incidents.ApplicationId = @appId", - " AND ErrorReports.ApplicationId = @appId"); - cmd.AddParameter("appId", query.ApplicationId); - cmd.AddParameter("minDate", startDate); - using (var reader = await cmd.ExecuteReaderAsync()) - { - var todayWithHour = DateTime.Today.AddHours(DateTime.Now.Hour); - while (await reader.ReadAsync()) - { - var hour = reader.GetInt32(0); - var date = hour < todayWithHour.Hour - ? DateTime.Today.AddHours(hour) - : DateTime.Today.AddDays(-1).AddHours(hour); - incidentValues[date] = reader.GetInt32(1); - } - await reader.NextResultAsync(); - while (await reader.ReadAsync()) - { - var hour = reader.GetInt32(0); - var date = hour < todayWithHour.Hour - ? DateTime.Today.AddHours(hour) - : DateTime.Today.AddDays(-1).AddHours(hour); - reportValues[date] = reader.GetInt32(1); - } - } - } - - result.ErrorReports = reportValues.Values.ToArray(); - result.Incidents = incidentValues.Values.ToArray(); - - //a bit weird, but required since the method - await GetStatSummary(query, result); - - return result; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Feedback/Commands/SubmitFeedbackHandler.cs b/src/Server/OneTrueError.SqlServer/Core/Feedback/Commands/SubmitFeedbackHandler.cs deleted file mode 100644 index 1038c0df..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Feedback/Commands/SubmitFeedbackHandler.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Data.Common; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using log4net; -using OneTrueError.Api.Core.Feedback.Commands; -using OneTrueError.Api.Core.Feedback.Events; -using OneTrueError.Api.Core.Reports; -using OneTrueError.App.Core.Reports; - -namespace OneTrueError.SqlServer.Core.Feedback.Commands -{ - [Component] - public class SubmitFeedbackHandler : ICommandHandler - { - private readonly ILog _logger = LogManager.GetLogger(typeof(SubmitFeedbackHandler)); - private readonly IReportsRepository _reportsRepository; - private readonly IAdoNetUnitOfWork _unitOfWork; - private readonly IEventBus _eventBus; - - public SubmitFeedbackHandler(IAdoNetUnitOfWork unitOfWork, IReportsRepository reportsRepository, - IEventBus eventBus) - { - _unitOfWork = unitOfWork; - _reportsRepository = reportsRepository; - _eventBus = eventBus; - } - - public async Task ExecuteAsync(SubmitFeedback command) - { - ReportDTO report; - if (command.ReportId > 0) - report = await _reportsRepository.GetAsync(command.ReportId); - else - report = await _reportsRepository.FindByErrorIdAsync(command.ErrorId); - - - // storing it without connections as the report might not have been uploaded yet. - if (report == null) - { - _logger.InfoFormat( - "Failed to find report. Let's enqueue it instead for report {0}/{1}. Email: {2}, Feedback: {3}", - command.ReportId, command.ErrorId, command.Email, command.Feedback); - try - { - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = "INSERT INTO IncidentFeedback (ErrorReportId, RemoteAddress, Description, EmailAddress, CreatedAtUtc, Conversation, ConversationLength) " - + - "VALUES (@ErrorReportId, @RemoteAddress, @Description, @EmailAddress, @CreatedAtUtc, '', 0)"; - cmd.AddParameter("ErrorReportId", command.ErrorId); - cmd.AddParameter("RemoteAddress", command.RemoteAddress); - cmd.AddParameter("Description", command.Feedback); - cmd.AddParameter("EmailAddress", command.Email); - cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow); - cmd.ExecuteNonQuery(); - } - } - catch (Exception exception) - { - _logger.Error( - string.Format("{0}: Failed to store '{1}' '{2}'", command.ErrorId, command.Email, - command.Feedback), exception); - //hide errors. - } - - return; - } - - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = "INSERT INTO IncidentFeedback (ErrorReportId, ApplicationId, ReportId, IncidentId, RemoteAddress, Description, EmailAddress, CreatedAtUtc, Conversation, ConversationLength) " - + - "VALUES (@ErrorReportId, @ApplicationId, @ReportId, @IncidentId, @RemoteAddress, @Description, @EmailAddress, @CreatedAtUtc, @Conversation, 0)"; - cmd.AddParameter("ErrorReportId", command.ErrorId); - cmd.AddParameter("ApplicationId", report.ApplicationId); - cmd.AddParameter("ReportId", report.Id); - cmd.AddParameter("IncidentId", report.IncidentId); - cmd.AddParameter("RemoteAddress", command.RemoteAddress); - cmd.AddParameter("Description", command.Feedback); - cmd.AddParameter("EmailAddress", command.Email); - cmd.AddParameter("Conversation", ""); - cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow); - - var evt = new FeedbackAttachedToIncident - { - Message = command.Feedback, - UserEmailAddress = command.Email, - IncidentId = report.IncidentId - }; - await _eventBus.PublishAsync(evt); - - await cmd.ExecuteNonQueryAsync(); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Feedback/FeedbackEntityMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Feedback/FeedbackEntityMapper.cs deleted file mode 100644 index 385edb41..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Feedback/FeedbackEntityMapper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.App.Core.Feedback; - -namespace OneTrueError.SqlServer.Core.Feedback -{ - public class FeedbackEntityMapper : CrudEntityMapper - { - public FeedbackEntityMapper() : base("IncidentFeedback") - { - Property(x => x.ErrorId).ColumnName("ErrorReportId"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Feedback/FeedbackRepository.cs b/src/Server/OneTrueError.SqlServer/Core/Feedback/FeedbackRepository.cs deleted file mode 100644 index 7daf4046..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Feedback/FeedbackRepository.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.App.Core.Feedback; - -namespace OneTrueError.SqlServer.Core.Feedback -{ - [Component] - public class FeedbackRepository : IFeedbackRepository - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public FeedbackRepository(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task FindPendingAsync(string reportId) - { - return await _unitOfWork.FirstOrDefaultAsync(new {ErrorId = reportId}); - } - - public async Task UpdateAsync(FeedbackEntity feedback) - { - await _unitOfWork.UpdateAsync(feedback); - } - - public async Task> GetEmailAddressesAsync(int incidentId) - { - var emailAddresses = new List(); - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = - "SELECT distinct EmailAddress FROM IncidentFeedback WHERE IncidentId = @id AND EmailAddress IS NOT NULL"; - cmd.AddParameter("id", incidentId); - using (var reader = await cmd.ExecuteReaderAsync()) - { - while (await reader.ReadAsync()) - { - emailAddresses.Add(reader.GetString(0)); - } - } - } - - return emailAddresses; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentMapper.cs deleted file mode 100644 index d8bef407..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentMapper.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.App.Core.Incidents; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Core.Incidents -{ - public class IncidentMapper : CrudEntityMapper - { - public IncidentMapper() : base("Incidents") - { - Property(x => x.SolvedAtUtc) - .ToColumnValue(DbConverters.ToNullableSqlDate) - .ToPropertyValue(DbConverters.ToEntityDate); - - Property(x => x.LastSolutionAtUtc) - .ToColumnValue(DbConverters.ToNullableSqlDate) - .ToPropertyValue(DbConverters.ToEntityDate); - - Property(x => x.IgnoringReportsSinceUtc) - .ToColumnValue(DbConverters.ToNullableSqlDate) - .ToPropertyValue(DbConverters.ToEntityDate); - - Property(x => x.Solution) - .ToColumnValue(DbConverters.SerializeEntity) - .ToPropertyValue(EntitySerializer.Deserialize); - - Property(x => x.IsSolved) - .ToPropertyValue(DbConverters.BoolFromByteArray); - - Property(x => x.IsSolutionShared) - .ToPropertyValue(DbConverters.BoolFromByteArray); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentRepository.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentRepository.cs deleted file mode 100644 index c3195e94..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/IncidentRepository.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Incidents; -using OneTrueError.App.Core.Incidents; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Core.Incidents -{ - [Component] - public class IncidentRepository : IIncidentRepository - { - private readonly IAdoNetUnitOfWork _uow; - - public IncidentRepository(IAdoNetUnitOfWork uow) - { - if (uow == null) throw new ArgumentNullException("uow"); - - _uow = uow; - } - - public async Task UpdateAsync(Incident incident) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = - @"UPDATE Incidents SET - ApplicationId = @ApplicationId, - UpdatedAtUtc = @UpdatedAtUtc, - Description = @Description, - Solution = @Solution, - IsSolved = @IsSolved, - IsSolutionShared = @IsSolutionShared, - IgnoreReports = @IgnoreReports, - IgnoringReportsSinceUtc = @IgnoringReportsSinceUtc, - IgnoringRequestedBy = @IgnoringRequestedBy - WHERE Id = @id"; - cmd.AddParameter("Id", incident.Id); - cmd.AddParameter("ApplicationId", incident.ApplicationId); - cmd.AddParameter("UpdatedAtUtc", incident.UpdatedAtUtc); - cmd.AddParameter("Description", incident.Description); - cmd.AddParameter("IgnoreReports", incident.IgnoreReports); - cmd.AddParameter("IgnoringReportsSinceUtc", incident.IgnoringReportsSinceUtc.ToDbNullable()); - cmd.AddParameter("IgnoringRequestedBy", incident.IgnoringRequestedBy); - cmd.AddParameter("Solution", - incident.Solution == null ? null : EntitySerializer.Serialize(incident.Solution)); - cmd.AddParameter("IsSolved", incident.IsSolved); - cmd.AddParameter("IsSolutionShared", incident.IsSolutionShared); - await cmd.ExecuteNonQueryAsync(); - } - } - - public async Task GetTotalCountForAppInfoAsync(int applicationId) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = - @"SELECT CAST(count(*) as int) FROM Incidents WHERE ApplicationId = @ApplicationId"; - cmd.AddParameter("ApplicationId", applicationId); - var result = (int) await cmd.ExecuteScalarAsync(); - return result; - } - } - - public Task GetAsync(int id) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = - "SELECT TOP 1 * FROM Incidents WHERE Id = @id"; - - cmd.AddParameter("id", id); - return cmd.FirstAsync(new IncidentMapper()); - } - } - - public Incident Find(int id) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = - "SELECT TOP 1 * FROM Incidents WHERE Id = @id"; - - cmd.AddParameter("id", id); - return cmd.FirstOrDefault(new IncidentMapper()); - } - } - - public IEnumerable FindLatestForApplication(int applicationId, int count) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = - "SELECT TOP " + count + - " * FROM Incidents WHERE ApplicationId = @applicationId AND IsSolved=0 ORDER BY UpdatedAtUtc DESC"; - - cmd.AddParameter("applicationId", applicationId); - return cmd.ToList(); - } - } - - public IEnumerable FindLatestForOrganization(int count) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = - "SELECT TOP " + count + " * FROM Incidents WHERE IsSolved=0 ORDER BY UpdatedAtUtc DESC"; - - - return cmd.ToList(); - } - } - - public IEnumerable FindWithMostReportsForOrganization(int count) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = - "SELECT TOP " + count + " * FROM Incidents WHERE IsSolved=0 ORDER BY Count DESC"; - - return cmd.ToList(); - } - } - - public Incident Get(int id) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = - "SELECT TOP 3 * FROM Incidents WHERE Id = @id"; - - cmd.AddParameter("id", id); - return cmd.First(new IncidentMapper()); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs deleted file mode 100644 index 6717cf55..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/FindIncidentResultItemMapper.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using System.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Incidents.Queries; - -namespace OneTrueError.SqlServer.Core.Incidents.Queries -{ - public class FindIncidentResultItemMapper : IEntityMapper - { - public object Create(IDataRecord record) - { - return new FindIncidentResultItem((int) record["Id"], (string) record["Description"]); - } - - public void Map(IDataRecord source, object destination) - { - Map(source, (FindIncidentResultItem) destination); - } - - public void Map(IDataRecord source, FindIncidentResultItem destination) - { - destination.ApplicationName = (string) source["ApplicationName"]; - destination.ApplicationId = source["ApplicationId"].ToString(); - destination.IsReOpened = source["IsReopened"].Equals(1); - destination.ReportCount = (int) source["ReportCount"]; - destination.LastUpdateAtUtc = (DateTime) source["UpdatedAtUtc"]; - destination.CreatedAtUtc = (DateTime) source["CreatedAtUtc"]; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/FindIncidentsHandler.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/FindIncidentsHandler.cs deleted file mode 100644 index 6b0b9c80..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/FindIncidentsHandler.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Data.Common; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Incidents; -using OneTrueError.Api.Core.Incidents.Queries; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.SqlServer.Core.Incidents.Queries -{ - [Component] - public class FindIncidentsHandler : IQueryHandler - { - private readonly IAdoNetUnitOfWork _uow; - - public FindIncidentsHandler(IAdoNetUnitOfWork uow) - { - _uow = uow; - } - - public async Task ExecuteAsync(FindIncidents query) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - var sqlQuery = @"SELECT {0} - FROM Incidents - JOIN Applications ON (Applications.Id = Incidents.ApplicationId) - JOIN ApplicationMembers atm ON (atm.ApplicationId = Applications.Id AND AccountId = @accountId)"; - cmd.AddParameter("accountId", ClaimsPrincipal.Current.GetAccountId()); - - if (query.ApplicationId > 0) - { - sqlQuery += " WHERE Applications.Id = @id"; - cmd.AddParameter("id", query.ApplicationId); - } - if (query.FreeText != null) - { - sqlQuery += @" AND ( - Incidents.Id IN (SELECT Distinct IncidentId FROM ErrorReports WHERE StackTrace LIKE @FreeText - Or Incidents.Description LIKE @FreeText) - )"; - cmd.AddParameter("FreeText", $"%{query.FreeText}%"); - } - - sqlQuery += " AND ("; - if (query.Ignored) - sqlQuery += "IgnoreReports = 1 OR "; - if (query.Closed) - sqlQuery += "IsSolved = 1 OR "; - if (query.Open) - sqlQuery += "(IsSolved = 0 AND IgnoreReports = 0) OR "; - if (query.ReOpened) - sqlQuery += "(IsReOpened = 1) OR "; - - if (sqlQuery.EndsWith("OR ")) - sqlQuery = sqlQuery.Remove(sqlQuery.Length - 4) + ") "; - else - sqlQuery = sqlQuery.Remove(sqlQuery.Length - 5); - - if (query.MinDate > DateTime.MinValue) - { - sqlQuery += " AND Incidents.UpdatedAtUtc >= @minDate"; - cmd.AddParameter("minDate", query.MinDate); - } - if (query.MaxDate < DateTime.MaxValue) - { - sqlQuery += " AND Incidents.UpdatedAtUtc <= @maxDate"; - cmd.AddParameter("maxDate", query.MaxDate); - } - - //count first; - cmd.CommandText = string.Format(sqlQuery, "count(Incidents.Id)"); - var count = await cmd.ExecuteScalarAsync(); - - - // then items - if (query.SortType == IncidentOrder.Newest) - { - if (query.SortAscending) - sqlQuery += " ORDER BY UpdatedAtUtc"; - else - sqlQuery += " ORDER BY UpdatedAtUtc DESC"; - } - else if (query.SortType == IncidentOrder.MostReports) - { - if (query.SortAscending) - sqlQuery += " ORDER BY ReportCount"; - else - sqlQuery += " ORDER BY ReportCount DESC"; - } - cmd.CommandText = string.Format(sqlQuery, - "Incidents.*, Applications.Id as ApplicationId, Applications.Name as ApplicationName"); - if (query.PageNumber > 0) - { - var offset = (query.PageNumber - 1)*query.ItemsPerPage; - cmd.CommandText += string.Format(@" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, - query.ItemsPerPage); - } - var items = await cmd.ToListAsync(); - - return new FindIncidentResult - { - Items = items.ToArray(), - PageNumber = query.PageNumber, - PageSize = query.ItemsPerPage, - TotalCount = (int) count - }; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentForClosePage.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentForClosePage.cs deleted file mode 100644 index aa0d6462..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentForClosePage.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Incidents.Queries; - -namespace OneTrueError.SqlServer.Core.Incidents.Queries -{ - [Component] - internal class GetIncidentForClosePageHandler : - IQueryHandler - { - private readonly IAdoNetUnitOfWork _uow; - - public GetIncidentForClosePageHandler(IAdoNetUnitOfWork uow) - { - _uow = uow; - } - - public async Task ExecuteAsync(GetIncidentForClosePage query) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = @"select Incidents.Description, -(select count(*) from IncidentFeedback WHERE IncidentFeedback.IncidentId = Incidents.Id AND IncidentFeedback.EmailAddress is not null AND IncidentFeedback.EmailAddress <> '') as SubscriberCount -FROM Incidents -WHERE Incidents.Id = @incidentId"; - cmd.AddParameter("incidentId", query.IncidentId); - return await cmd.FirstAsync(); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs deleted file mode 100644 index efc94b8c..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentForClosePageResultMapper.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Incidents.Queries; - -namespace OneTrueError.SqlServer.Core.Incidents.Queries -{ - public class GetIncidentForClosePageResultMapper : EntityMapper - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs deleted file mode 100644 index 8ebf33d3..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentHandler.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Incidents.Queries; - -namespace OneTrueError.SqlServer.Core.Incidents.Queries -{ - [Component] - public class GetIncidentHandler : IQueryHandler - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public GetIncidentHandler(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task ExecuteAsync(GetIncident query) - { - var result = await _unitOfWork.FirstAsync(new {Id = query.IncidentId}); - - var tags = GetTags(query.IncidentId); - result.Tags = tags.ToArray(); - - await GetContextCollectionNames(result); - await GetReportStatistics(result); - await GetStatSummary(query, result); - return result; - } - - //TODO : Do not mess with the similarity tables directly - private async Task GetContextCollectionNames(GetIncidentResult result) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = @"select distinct Name -from [IncidentContextCollections] -where IncidentId=@incidentId"; - cmd.AddParameter("incidentId", result.Id); - using (var reader = await cmd.ExecuteReaderAsync()) - { - var names = new List(); - while (await reader.ReadAsync()) - { - names.Add(reader.GetString(0)); - } - result.ContextCollections = names.ToArray(); - } - } - } - - private async Task GetReportStatistics(GetIncidentResult result) - { - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = @"select cast(createdatutc as date) as Date, count(*) as Count -from errorreports -where incidentid=@incidentId -AND CreatedAtUtc > @date -group by cast(createdatutc as date)"; - var startDate = DateTime.Today.AddDays(-29); - cmd.AddParameter("date", startDate); - cmd.AddParameter("incidentId", result.Id); - var specifiedDays = await cmd.ToListAsync(); - var curDate = startDate; - var values = new ReportDay[30]; - var valuesIndexer = 0; - var specifiedDaysIndexer = 0; - while (curDate <= DateTime.Today) - { - if (specifiedDays.Count > specifiedDaysIndexer && - specifiedDays[specifiedDaysIndexer].Date == curDate) - values[valuesIndexer++] = specifiedDays[specifiedDaysIndexer++]; - else - values[valuesIndexer++] = new ReportDay {Date = curDate}; - curDate = curDate.AddDays(1); - } - result.DayStatistics = values; - } - } - - private async Task GetStatSummary(GetIncident query, GetIncidentResult result) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = @" -select count(distinct emailaddress) from IncidentFeedback -where @minDate < CreatedAtUtc -AND emailaddress is not null -AND DATALENGTH(emailaddress) > 0 -AND IncidentId = @incidentId; -select count(*) from IncidentFeedback -where @minDate < CreatedAtUtc -AND Description is not null -AND DATALENGTH(Description) > 0 -AND IncidentId = @incidentId;"; - cmd.AddParameter("incidentId", query.IncidentId); - cmd.AddParameter("minDate", DateTime.Today.AddDays(-90)); - - using (var reader = await cmd.ExecuteReaderAsync()) - { - if (!await reader.ReadAsync()) - { - throw new InvalidOperationException("Expected to be able to read result 1."); - } - - result.WaitingUserCount = reader.GetInt32(0); - - await reader.NextResultAsync(); - if (!await reader.ReadAsync()) - { - throw new InvalidOperationException("Expected to be able to read result 2."); - } - - result.FeedbackCount = reader.GetInt32(0); - } - } - } - - private List GetTags(int incidentId) - { - var tags = new List(); - using (var cmd = _unitOfWork.CreateCommand()) - { - cmd.CommandText = "SELECT TagName FROM IncidentTags WHERE IncidentId=@id"; - cmd.AddParameter("id", incidentId); - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - tags.Add((string) reader[0]); - } - } - } - return tags; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentResultMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentResultMapper.cs deleted file mode 100644 index d4406dfb..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetIncidentResultMapper.cs +++ /dev/null @@ -1,30 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Incidents.Queries; -using OneTrueError.App.Core.Incidents; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Core.Incidents.Queries -{ - public class GetIncidentResultMapper : CrudEntityMapper - { - public GetIncidentResultMapper() - : base("Incidents") - { - Property(x => x.DayStatistics).Ignore(); - Property(x => x.WaitingUserCount).Ignore(); - Property(x => x.FeedbackCount).Ignore(); - Property(x => x.Tags).Ignore(); - Property(x => x.ContextCollections).Ignore(); - Property(x => x.IsIgnored).ColumnName("IgnoreReports"); - - Property(x => x.Solution) - .ToPropertyValue(x => EntitySerializer.Deserialize(x)?.Description); - - Property(x => x.IsSolved) - .ToPropertyValue(DbConverters.BoolFromByteArray); - - Property(x => x.IsSolutionShared) - .ToPropertyValue(DbConverters.BoolFromByteArray); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetReportListResultItemMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetReportListResultItemMapper.cs deleted file mode 100644 index 10085e51..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/Queries/GetReportListResultItemMapper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Reports.Queries; - -namespace OneTrueError.SqlServer.Core.Incidents.Queries -{ - public class GetReportListResultItemMapper : EntityMapper - { - public GetReportListResultItemMapper() - { - Property(x => x.Message).ColumnName("Title"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Incidents/ReportDayMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Incidents/ReportDayMapper.cs deleted file mode 100644 index 47545c2d..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Incidents/ReportDayMapper.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Incidents.Queries; - -namespace OneTrueError.SqlServer.Core.Incidents -{ - internal class ReportDayMapper : EntityMapper - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Invitations/GetInvitationByKeyHandler.cs b/src/Server/OneTrueError.SqlServer/Core/Invitations/GetInvitationByKeyHandler.cs deleted file mode 100644 index 845713b5..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Invitations/GetInvitationByKeyHandler.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using OneTrueError.Api.Core.Invitations.Queries; - -namespace OneTrueError.SqlServer.Core.Invitations -{ - [Component] - internal class GetInvitationByKeyHandler : IQueryHandler - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public GetInvitationByKeyHandler(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task ExecuteAsync(GetInvitationByKey query) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = "SELECT email FROM Invitations WHERE InvitationKey = @id"; - cmd.AddParameter("id", query.InvitationKey); - return new GetInvitationByKeyResult {EmailAddress = (string) await cmd.ExecuteScalarAsync()}; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Invitations/InvitationMapping.cs b/src/Server/OneTrueError.SqlServer/Core/Invitations/InvitationMapping.cs deleted file mode 100644 index 5e4c0993..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Invitations/InvitationMapping.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Collections.Generic; -using Griffin.Data.Mapper; -using OneTrueError.App.Core.Invitations; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Core.Invitations -{ - public class InvitationMapping : CrudEntityMapper - { - public InvitationMapping() - : base("Invitations") - { - Property(x => x.Id).PrimaryKey(true); - Property(x => x.EmailToInvitedUser).ColumnName("Email"); - Property(x => x.Invitations) - .ToColumnValue(EntitySerializer.Serialize) - .ToPropertyValue(x => EntitySerializer.Deserialize>((string) x)); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Notifications/NotificationRepository.cs b/src/Server/OneTrueError.SqlServer/Core/Notifications/NotificationRepository.cs deleted file mode 100644 index eed3fbe0..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Notifications/NotificationRepository.cs +++ /dev/null @@ -1,82 +0,0 @@ -using System.Collections.Generic; -using System.Data.Common; -using System.Linq; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.App.Core.Notifications; - -namespace OneTrueError.SqlServer.Core.Notifications -{ - [Component] - public class NotificationRepository : INotificationsRepository - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public NotificationRepository(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task TryGetAsync(int accountId, int applicationId) - { - if (applicationId == 0) - applicationId = -1; - return await _unitOfWork.FirstOrDefaultAsync( - new - { - AccountId = accountId, - ApplicationId = applicationId - }); - } - - public async Task UpdateAsync(UserNotificationSettings notificationSettings) - { - if (notificationSettings.ApplicationId == 0) - notificationSettings.ApplicationId = -1; - await _unitOfWork.UpdateAsync(notificationSettings); - } - - public async Task CreateAsync(UserNotificationSettings notificationSettings) - { - if (notificationSettings.ApplicationId == 0) - notificationSettings.ApplicationId = -1; - await _unitOfWork.InsertAsync(notificationSettings); - } - - public async Task ExistsAsync(int accountId, int applicationId) - { - if (applicationId == 0) - applicationId = -1; - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = - "SELECT TOP 1 AccountId FROM UserNotificationSettings WHERE AccountId = @id AND ApplicationId = @appId"; - cmd.AddParameter("id", accountId); - cmd.AddParameter("appId", applicationId); - return await cmd.ExecuteScalarAsync() != null; - } - } - - public async Task> GetAllAsync(int applicationId) - { - var sql = - @"SELECT * - FROM UserNotificationSettings - WHERE ApplicationId = @1 - OR ApplicationId = -1 - ORDER By AccountId, ApplicationId DESC"; - var settings = await _unitOfWork.ToListAsync(sql, applicationId); - var dict = new Dictionary(); - foreach (var setting in settings) - { - if (dict.ContainsKey(setting.AccountId)) - continue; - dict[setting.AccountId] = setting; - } - - return dict.Values.ToList(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Reports/ErrorReportDtoMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Reports/ErrorReportDtoMapper.cs deleted file mode 100644 index a25bb6f3..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Reports/ErrorReportDtoMapper.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Reports; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Core.Reports -{ - public class ErrorReportDtoMapper : CrudEntityMapper - { - public ErrorReportDtoMapper() - : base("ErrorReports") - { - Property(x => x.ContextCollections) - .ColumnName("ContextInfo") - .ToColumnValue(EntitySerializer.Serialize) - .ToPropertyValue(colValue => EntitySerializer.Deserialize((string) colValue)); - - Property(x => x.ReportVersion).Ignore(); - - Property(x => x.Exception) - .ToPropertyValue(EntitySerializer.Deserialize) - .ToColumnValue(EntitySerializer.Serialize); - - Property(x => x.ReportId) - .ColumnName("ErrorId"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Reports/ErrorReportRepository.cs b/src/Server/OneTrueError.SqlServer/Core/Reports/ErrorReportRepository.cs deleted file mode 100644 index 85bacd7e..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Reports/ErrorReportRepository.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Core.Reports; -using OneTrueError.App.Core.Reports; -using OneTrueError.App.Core.Reports.Invalid; - -namespace OneTrueError.SqlServer.Core.Reports -{ - [Component] - internal class ErrorReportRepository : IReportsRepository - { - private readonly IAdoNetUnitOfWork _uow; - - public ErrorReportRepository(IAdoNetUnitOfWork uow) - { - if (uow == null) throw new ArgumentNullException("uow"); - - _uow = uow; - } - - public async Task CreateAsync(InvalidErrorReport entity) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = - @"INSERT INTO InvalidErrorReports (Id, AddedAtUtc, ApplicationId, Body, Exception) VALUES(@Id, @AddedAtUtc, @OrganizationId, @ApplicationId, @Body, @Exception)"; - cmd.AddParameter("Id", entity.Id); - cmd.AddParameter("AddedAtUtc", entity.AddedAtUtc); - cmd.AddParameter("ApplicationId", entity.ApplicationId); - cmd.AddParameter("Body", entity.Report); - cmd.AddParameter("Exception", entity.Exception); - await cmd.ExecuteNonQueryAsync(); - } - } - - public async Task GetAsync(int id) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = - "SELECT * FROM ErrorReports WHERE Id = @id"; - - cmd.AddParameter("id", id); - return await cmd.FirstOrDefaultAsync(); - } - } - - public async Task FindByErrorIdAsync(string errorId) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = - "SELECT * FROM ErrorReports WHERE ErrorId = @id"; - - cmd.AddParameter("id", errorId); - return await cmd.FirstOrDefaultAsync(); - } - } - - public async Task GetForIncidentAsync(int incidentId, int pageNumber, int pageSize) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.AddParameter("incidentId", incidentId); - long totalRows = 0; - if (pageNumber > 0) - { - cmd.CommandText = - "SELECT count(*) FROM ErrorReports WHERE IncidentId = @incidentId"; - totalRows = (int) await cmd.ExecuteScalarAsync(); - } - - cmd.CommandText = - "SELECT * FROM ErrorReports WHERE IncidentId = @incidentId ORDER BY CreatedAtUtc DESC"; - if (pageNumber > 0) - { - var offset = (pageNumber - 1)*pageSize; - cmd.CommandText += string.Format(@" OFFSET {0} ROWS FETCH NEXT {1} ROWS ONLY", offset, pageSize); - } - - //cmd.AddParameter("incidentId", incidentId); - var list = await cmd.ToListAsync(); - return new PagedReports - { - TotalCount = (int) totalRows, - Reports = (IReadOnlyList) list - }; - } - } - - [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities")] - public IEnumerable GetAll(int[] ids) - { - //TODO: Remove SQL injection vulnerability - - using (var cmd = _uow.CreateCommand()) - { - var idString = string.Join(",", ids.Select(x => "'" + x + "'")); - - cmd.CommandText = - string.Format( - "SELECT * FROM ErrorReports WHERE Id IN ({0})", - idString); - - return cmd.ToList().ToList(); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Core/Users/UserMapper.cs b/src/Server/OneTrueError.SqlServer/Core/Users/UserMapper.cs deleted file mode 100644 index 5768d10f..00000000 --- a/src/Server/OneTrueError.SqlServer/Core/Users/UserMapper.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.App.Core.Users; - -namespace OneTrueError.SqlServer.Core.Users -{ - public class UserMapper : CrudEntityMapper - { - public UserMapper() : base("Users") - { - Property(x => x.AccountId).PrimaryKey(false); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Geolocation/ErrorOriginRepository.cs b/src/Server/OneTrueError.SqlServer/Modules/Geolocation/ErrorOriginRepository.cs deleted file mode 100644 index f9b04111..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Geolocation/ErrorOriginRepository.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using OneTrueError.App.Modules.Geolocation; - -namespace OneTrueError.SqlServer.Modules.Geolocation -{ - [Component] - public class ErrorOriginRepository : IErrorOriginRepository - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public ErrorOriginRepository(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task CreateAsync(ErrorOrigin command, int applicationId, int incidentId, int reportId) - { - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = "SELECT Id FROM ErrorOrigins WHERE IpAddress = @ip"; - cmd.AddParameter("ip", command.IpAddress); - var id = await cmd.ExecuteScalarAsync(); - if (id is int) - { - await CreateReportInfoAsync((int) id, applicationId, incidentId, reportId); - return; - } - } - - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = "INSERT INTO ErrorOrigins (IpAddress, CountryCode, CountryName, RegionCode, RegionName, City, ZipCode, Latitude, Longitude, CreatedAtUtc) " - + - "VALUES (@IpAddress, @CountryCode, @CountryName, @RegionCode, @RegionName, @City, @ZipCode, @Latitude, @Longitude, @CreatedAtUtc);select cast(SCOPE_IDENTITY() as int);"; - cmd.AddParameter("IpAddress", command.IpAddress); - cmd.AddParameter("CountryCode", command.CountryCode); - cmd.AddParameter("CountryName", command.CountryName); - cmd.AddParameter("RegionCode", command.RegionCode); - cmd.AddParameter("RegionName", command.RegionName); - cmd.AddParameter("City", command.City); - cmd.AddParameter("ZipCode", command.ZipCode); - //cmd.AddParameter("Point", SqlGeography.Point(command.Latitude, command.Longitude, 4326)); - cmd.AddParameter("Latitude", command.Latitude); - cmd.AddParameter("Longitude", command.Longitude); - cmd.AddParameter("CreatedAtUtc", DateTime.UtcNow); - var id = (int) await cmd.ExecuteScalarAsync(); - await CreateReportInfoAsync(id, applicationId, incidentId, reportId); - } - } - - public async Task> FindForIncidentAsync(int incidentId) - { - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = @"SELECT Longitude, Latitude, count(*) - FROM ErrorOrigins eo - JOIN ErrorReportOrigins ON (eo.Id = ErrorReportOrigins.ErrorOriginId) - WHERE IncidentId = @id - GROUP BY IncidentId, Longitude, Latitude"; - cmd.AddParameter("id", incidentId); - using (var reader = await cmd.ExecuteReaderAsync()) - { - var items = new List(); - while (await reader.ReadAsync()) - { - var item = new ErrorOrginListItem - { - Longitude = (double) reader.GetDecimal(0), - Latitude = (double) reader.GetDecimal(1), - NumberOfErrorReports = reader.GetInt32(2) - }; - items.Add(item); - } - return items; - } - } - } - - private async Task CreateReportInfoAsync(int originId, int applicationId, int incidentId, int reportId) - { - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = - "INSERT INTO ErrorReportOrigins (ErrorOriginId, ApplicationId, IncidentId, ReportId, CreatedAtUtc) VALUES(@oid, @aid, @iid, @rid, GetUtcDate())"; - cmd.AddParameter("oid", originId); - cmd.AddParameter("aid", applicationId); - cmd.AddParameter("iid", incidentId); - cmd.AddParameter("rid", reportId); - await cmd.ExecuteNonQueryAsync(); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/ReportSpikes/ReportSpikesRepository.cs b/src/Server/OneTrueError.SqlServer/Modules/ReportSpikes/ReportSpikesRepository.cs deleted file mode 100644 index 0485aa73..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/ReportSpikes/ReportSpikesRepository.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Data.Common; -using System.Linq; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.App.Modules.ReportSpikes; - -namespace OneTrueError.SqlServer.Modules.ReportSpikes -{ - [Component] - public class ReportSpikesRepository : IReportSpikeRepository - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public ReportSpikesRepository(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public virtual async Task GetAverageReportCountAsync(int applicationId) - { - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = @"SELECT - [Day] = DATENAME(WEEKDAY, createdatutc), - Totals = cast (COUNT(*) as int) - FROM errorreports - WHERE applicationid=@appId - GROUP BY - DATENAME(WEEKDAY, createdatutc)"; - cmd.AddParameter("appId", applicationId); - var numbers = new List(); - using (var reader = await cmd.ExecuteReaderAsync()) - { - while (await reader.ReadAsync()) - { - numbers.Add((int) reader[1]); - } - } - numbers.Sort(); - - RemovePeaks(numbers); - return (int) numbers.Average(); - } - } - - public async Task GetSpikeAsync(int applicationId) - { - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = @"SELECT * FROM ErrorReportSpikes WHERE ApplicationId = @appId AND SpikeDate = @date"; - cmd.AddParameter("date", DateTime.Today); - cmd.AddParameter("appId", applicationId); - return await cmd.FirstOrDefaultAsync(); - } - } - - public async Task GetTodaysCountAsync(int applicationId) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = - @"SELECT count(*) FROM ErrorReports WHERE ApplicationId = @appId AND CreatedAtUtc >= @date"; - cmd.AddParameter("date", DateTime.UtcNow.AddHours(-24)); - cmd.AddParameter("appId", applicationId); - return (int) await cmd.ExecuteScalarAsync(); - } - } - - public async Task CreateSpikeAsync(ErrorReportSpike spike) - { - await _unitOfWork.InsertAsync(spike); - } - - public async Task UpdateSpikeAsync(ErrorReportSpike spike) - { - await _unitOfWork.UpdateAsync(spike); - } - - private static void RemovePeaks(IList numbers) - { - if (numbers.Count > 3) - { - numbers.RemoveAt(0); - numbers.RemoveAt(numbers.Count - 1); - } - if (numbers.Count > 3) - { - numbers.RemoveAt(0); - numbers.RemoveAt(numbers.Count - 1); - } - if (numbers.Count > 3) - { - numbers.RemoveAt(0); - numbers.RemoveAt(numbers.Count - 1); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs b/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs deleted file mode 100644 index 7a0ba503..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityCollectionMapper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.App.Modules.Similarities.Domain; - -namespace OneTrueError.SqlServer.Modules.Similarities.Mappers -{ - public class SimilarityCollectionMapper : CrudEntityMapper - { - public SimilarityCollectionMapper() - : base("SimilarityCollections") - { - Property(x => x.Id) - .PrimaryKey(true); - - Property(x => x.Properties) - .NotForCrud() - .NotForQueries(); - - Property(x => x.Name) - .ColumnName("ContextName"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityMapper.cs b/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityMapper.cs deleted file mode 100644 index 4b43b1ea..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityMapper.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.App.Modules.Similarities.Domain; - -namespace OneTrueError.SqlServer.Modules.Similarities.Mappers -{ - public class SimilarityMapper : CrudEntityMapper - { - public SimilarityMapper() - : base("Similarities") - { - Property(x => x.Id) - .PrimaryKey(true); - - Property(x => x.PropertyName) - .ColumnName("Name"); - - Property(x => x.Values) - .NotForCrud() - .NotForQueries(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityValueMapper.cs b/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityValueMapper.cs deleted file mode 100644 index dc61bbc2..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Similarities/Mappers/SimilarityValueMapper.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.App.Modules.Similarities.Domain; - -namespace OneTrueError.SqlServer.Modules.Similarities.Mappers -{ - public class SimilarityValueMapper : CrudEntityMapper - { - public SimilarityValueMapper() - : base("SimilarityValues") - { - //Property(x => x.Id).PrimaryKey(true); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Similarities/SimilarityRepository.cs b/src/Server/OneTrueError.SqlServer/Modules/Similarities/SimilarityRepository.cs deleted file mode 100644 index 70ff6bee..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Similarities/SimilarityRepository.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using log4net; -using OneTrueError.App.Modules.Similarities.Domain; -using OneTrueError.Infrastructure; -using OneTrueError.SqlServer.Modules.Similarities.Entities; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Modules.Similarities -{ - [Component] - public class SimilarityRepository : ISimilarityRepository - { - private readonly IAdoNetUnitOfWork _uow; - private ILog _logger = LogManager.GetLogger(typeof(SimilarityRepository)); - - public SimilarityRepository(IAdoNetUnitOfWork uow) - { - if (uow == null) throw new ArgumentNullException("uow"); - _uow = uow; - } - - public async Task CreateAsync(SimilaritiesReport similarity) - { - foreach (var collection in similarity.Collections) - { - var dto = collection.Properties.Select(x => new ContextCollectionPropertyDbEntity(x)).ToArray(); - var json = EntitySerializer.Serialize(dto); - if (collection.Id == 0) - { - using (var cmd = _uow.CreateDbCommand()) - { - cmd.CommandText = - @"INSERT INTO IncidentContextCollections (IncidentId, Name, Properties) - VALUES(@incidentId, @name, @props)"; - cmd.AddParameter("incidentId", collection.IncidentId); - cmd.AddParameter("name", collection.Name); - cmd.AddParameter("props", json); - await cmd.ExecuteNonQueryAsync(); - } - } - else - { - using (var cmd = _uow.CreateDbCommand()) - { - cmd.CommandText = - @"UPDATE IncidentContextCollections SET Properties=@props WHERE Id = @id"; - cmd.AddParameter("id", collection.Id); - cmd.AddParameter("props", json); - await cmd.ExecuteNonQueryAsync(); - } - } - } - } - - public SimilaritiesReport FindForIncident(int incidentId) - { - using (var cmd = _uow.CreateCommand()) - { - cmd.CommandText = - @"select Id, Name, Properties from IncidentContextCollections - where IncidentId = @incidentId"; - cmd.AddParameter("incidentId", incidentId); - - var collections = new List(); - - using (var reader = cmd.ExecuteReader()) - { - while (reader.Read()) - { - var json = (string) reader["Properties"]; - var properties = OneTrueSerializer.Deserialize(json); - var col = new SimilarityCollection(incidentId, reader.GetString(1)); - col.SetId(reader.GetInt32(0)); - foreach (var entity in properties) - { - var prop = new Similarity(entity.Name); - prop.LoadValues( - entity.Values.Select(x => new SimilarityValue(x.Value, x.Percentage, x.Count)).ToArray()); - col.Properties.Add(prop); - } - collections.Add(col); - } - } - - return collections.Count == 0 ? null : new SimilaritiesReport(incidentId, collections); - } - } - - public async Task UpdateAsync(SimilaritiesReport similarity) - { - await CreateAsync(similarity); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Tagging/TagsRepository.cs b/src/Server/OneTrueError.SqlServer/Modules/Tagging/TagsRepository.cs deleted file mode 100644 index 25d526d2..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Tagging/TagsRepository.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using OneTrueError.App.Modules.Tagging; -using OneTrueError.App.Modules.Tagging.Domain; - -namespace OneTrueError.SqlServer.Modules.Tagging -{ - [Component] - public class TagsRepository : ITagsRepository - { - private readonly IAdoNetUnitOfWork _adoNetUnitOfWork; - - public TagsRepository(IAdoNetUnitOfWork adoNetUnitOfWork) - { - _adoNetUnitOfWork = adoNetUnitOfWork; - } - - public async Task AddAsync(int incidentId, Tag[] tags) - { - foreach (var tag in tags) - { - using (var cmd = _adoNetUnitOfWork.CreateDbCommand()) - { - cmd.CommandText = - "INSERT INTO IncidentTags (IncidentId, TagName, OrderNumber) VALUES(@incidentId, @name, @orderNumber)"; - cmd.AddParameter("incidentId", incidentId); - cmd.AddParameter("name", tag.Name); - cmd.AddParameter("orderNumber", tag.OrderNumber); - await cmd.ExecuteNonQueryAsync(); - } - } - } - - public async Task> GetTagsAsync(int incidentId) - { - using (var cmd = _adoNetUnitOfWork.CreateDbCommand()) - { - cmd.CommandText = "SELECT * FROM IncidentTags WHERE IncidentId = @id ORDER BY OrderNumber"; - cmd.AddParameter("id", incidentId); - using (var reader = await cmd.ExecuteReaderAsync()) - { - var tags = new List(); - while (await reader.ReadAsync()) - { - var tag = new Tag((string) reader["TagName"], (int) reader["OrderNumber"]); - tags.Add(tag); - } - return tags; - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Triggers/CollectionMetadataMapper.cs b/src/Server/OneTrueError.SqlServer/Modules/Triggers/CollectionMetadataMapper.cs deleted file mode 100644 index d5e86e4e..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Triggers/CollectionMetadataMapper.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using Griffin.Data.Mapper; -using OneTrueError.App.Modules.Triggers.Domain; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Modules.Triggers -{ - public class CollectionMetadataMapper : CrudEntityMapper - { - public CollectionMetadataMapper() : base("CollectionMetaData") - { - Property(x => x.IsUpdated).Ignore(); - - Property(x => x.Properties) - .ToPropertyValue(EntitySerializer.Deserialize>); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Triggers/DeleteTriggerHandler.cs b/src/Server/OneTrueError.SqlServer/Modules/Triggers/DeleteTriggerHandler.cs deleted file mode 100644 index 23db5aa5..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Triggers/DeleteTriggerHandler.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Data.Common; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using OneTrueError.Api.Modules.Triggers.Commands; - -namespace OneTrueError.SqlServer.Modules.Triggers -{ - [Component] - public class DeleteTriggerHandler : ICommandHandler - { - private readonly IAdoNetUnitOfWork _uow; - - public DeleteTriggerHandler(IAdoNetUnitOfWork uow) - { - _uow = uow; - } - - public async Task ExecuteAsync(DeleteTrigger command) - { - using (var cmd = (DbCommand) _uow.CreateCommand()) - { - cmd.CommandText = "DELETE FROM Triggers WHERE Id = @id"; - cmd.AddParameter("id", command.Id); - await cmd.ExecuteNonQueryAsync(); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Triggers/GetTriggersForApplicationHandler.cs b/src/Server/OneTrueError.SqlServer/Modules/Triggers/GetTriggersForApplicationHandler.cs deleted file mode 100644 index 35a3f172..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Triggers/GetTriggersForApplicationHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Data.Common; -using System.Linq; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.Api.Modules.Triggers; -using OneTrueError.Api.Modules.Triggers.Queries; - -namespace OneTrueError.SqlServer.Modules.Triggers -{ - [Component] - public class GetTriggersForApplicationHandler : IQueryHandler - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public GetTriggersForApplicationHandler(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - public async Task ExecuteAsync(GetTriggersForApplication query) - { - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = "SELECT * FROM Triggers WHERE [ApplicationId]=@appId"; - cmd.AddParameter("appId", query.ApplicationId); - return (await cmd.ToListAsync()).ToArray(); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerDtoMapper.cs b/src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerDtoMapper.cs deleted file mode 100644 index ab7de586..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerDtoMapper.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.Api.Modules.Triggers; - -namespace OneTrueError.SqlServer.Modules.Triggers -{ - public class TriggerDtoMapper : EntityMapper - { - public TriggerDtoMapper() - { - Property(x => x.Id) - .ToPropertyValue(x => x.ToString()); - - Property(x => x.Summary) - .Ignore(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerRepository.cs b/src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerRepository.cs deleted file mode 100644 index da8f2907..00000000 --- a/src/Server/OneTrueError.SqlServer/Modules/Triggers/TriggerRepository.cs +++ /dev/null @@ -1,141 +0,0 @@ -using System.Collections.Generic; -using System.Data.Common; -using System.Linq; -using System.Threading.Tasks; -using Griffin.Container; -using Griffin.Data; -using Griffin.Data.Mapper; -using OneTrueError.App.Modules.Triggers.Domain; -using OneTrueError.SqlServer.Tools; - -namespace OneTrueError.SqlServer.Modules.Triggers -{ - [Component] - public class TriggerRepository : ITriggerRepository - { - private readonly IAdoNetUnitOfWork _unitOfWork; - - public TriggerRepository(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - - public async Task> GetCollectionsAsync(int applicationId) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = - "SELECT * FROM CollectionMetadata WHERE ApplicationId = @id"; - - cmd.AddParameter("id", applicationId); - return await cmd.ToListAsync(new CollectionMetadataMapper()); - } - } - - - public async Task UpdateAsync(CollectionMetadata collection) - { - var props = EntitySerializer.Serialize(collection.Properties); - - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = @"UPDATE CollectionMetadata SET Properties = @Properties - WHERE Id = @id"; - - cmd.AddParameter("Id", collection.Id); - cmd.AddParameter("Properties", props); - await cmd.ExecuteNonQueryAsync(); - } - } - - public async Task CreateAsync(CollectionMetadata entity) - { - var props = EntitySerializer.Serialize(entity.Properties); - - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = - @"INSERT INTO CollectionMetadata (Name, ApplicationId, Properties) VALUES(@Name, @ApplicationId, @Properties)"; - cmd.AddParameter("Name", entity.Name); - cmd.AddParameter("ApplicationId", entity.ApplicationId); - cmd.AddParameter("Properties", props); - await cmd.ExecuteNonQueryAsync(); - } - } - - - public async Task CreateAsync(Trigger trigger) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = - "INSERT INTO Triggers (ApplicationId, Name, Description, Rules, Actions, LastTriggerAction, RunForNewIncidents, RunForExistingIncidents) " + - "VALUES(@ApplicationId, @Name, @Description, @Rules, @Actions, @LastTriggerAction, @RunForNewIncidents, @RunForExistingIncidents)"; - cmd.AddParameter("ApplicationId", trigger.ApplicationId); - cmd.AddParameter("Name", trigger.Name); - cmd.AddParameter("Description", trigger.Description); - cmd.AddParameter("Rules", EntitySerializer.Serialize(trigger.Rules)); - cmd.AddParameter("Actions", EntitySerializer.Serialize(trigger.Actions)); - cmd.AddParameter("LastTriggerAction", trigger.LastTriggerAction); - cmd.AddParameter("RunForNewIncidents", trigger.RunForNewIncidents); - cmd.AddParameter("RunForExistingIncidents", trigger.RunForExistingIncidents); - await cmd.ExecuteNonQueryAsync(); - } - } - - public IEnumerable GetForApplication(int applicationId) - { - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = - "SELECT * FROM Triggers WHERE ApplicationId = @applicationId"; - cmd.AddParameter("applicationId", applicationId); - return cmd.ToList(new TriggerMapper()).ToList(); - } - } - - - public async Task GetAsync(int id) - { - using (var cmd = (DbCommand) _unitOfWork.CreateCommand()) - { - cmd.CommandText = - "SELECT * FROM Triggers WHERE Id = @id"; - cmd.AddParameter("id", id); - return await cmd.FirstAsync(new TriggerMapper()); - } - } - - public async Task UpdateAsync(Trigger trigger) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = - "UPDATE Triggers SET Name=@Name, Description=@Description, Rules=@Rules, Actions=@Actions, LastTriggerAction=@LastTriggerAction, RunForNewIncidents = @RunForNewIncidents, RunForExistingIncidents=@RunForExistingIncidents " + - " WHERE Id=@Id"; - cmd.AddParameter("Id", trigger.Id); - cmd.AddParameter("Name", trigger.Name); - cmd.AddParameter("Description", trigger.Description); - cmd.AddParameter("Rules", EntitySerializer.Serialize(trigger.Rules)); - cmd.AddParameter("Actions", EntitySerializer.Serialize(trigger.Actions)); - cmd.AddParameter("LastTriggerAction", trigger.LastTriggerAction); - cmd.AddParameter("RunForNewIncidents", trigger.RunForNewIncidents); - cmd.AddParameter("RunForExistingIncidents", trigger.RunForExistingIncidents); - await cmd.ExecuteNonQueryAsync(); - } - } - - public async Task DeleteAsync(int id) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = - "DELETE FROM Triggers" + - " WHERE Id=@Id"; - cmd.AddParameter("Id", id); - await cmd.ExecuteNonQueryAsync(); - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/OneTrueError.SqlServer.csproj b/src/Server/OneTrueError.SqlServer/OneTrueError.SqlServer.csproj deleted file mode 100644 index 9f1cc577..00000000 --- a/src/Server/OneTrueError.SqlServer/OneTrueError.SqlServer.csproj +++ /dev/null @@ -1,188 +0,0 @@ - - - - Debug - AnyCPU - Properties - OneTrueError.SqlServer - 512 - Library - {B967BEEA-CDDD-4A83-A4F2-1C946099ED51} - OneTrueError.SqlServer - v4.5.2 - - - true - full - DEBUG;TRACE - - prompt - false - bin\Debug\ - 4 - - - pdbonly - TRACE - prompt - true - bin\Release\ - 4 - - - - OneTrueError.Api - {fc331a95-fca4-4764-8004-0884665dd01f} - - - OneTrueError.App - {5ef42a74-9323-49fa-a1f6-974d6de77202} - - - OneTrueError.Infrastructure - {A78A50DA-C9D7-47F2-8528-D7EE39D91924} - - - OneTrueError.ReportAnalyzer - {29FBF805-CAFD-426A-A576-9756D375BF18} - - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.39\lib\net45\Griffin.Core.dll - True - - - ..\packages\log4net.2.0.5\lib\net45-full\log4net.dll - True - - - - ..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/OneTrueError.SqlServer.csproj.orig b/src/Server/OneTrueError.SqlServer/OneTrueError.SqlServer.csproj.orig deleted file mode 100644 index 1237f735..00000000 --- a/src/Server/OneTrueError.SqlServer/OneTrueError.SqlServer.csproj.orig +++ /dev/null @@ -1,171 +0,0 @@ - - - - - Debug - AnyCPU - {B967BEEA-CDDD-4A83-A4F2-1C946099ED51} - Library - Properties - OneTrueError.SqlServer - OneTrueError.SqlServer - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - ..\packages\DotNetCqs.1.0.0\lib\net45\DotNetCqs.dll - True - - - ..\packages\Griffin.Container.1.1.2\lib\net40\Griffin.Container.dll - True - - - ..\packages\Griffin.Framework.1.0.36\lib\net45\Griffin.Core.dll - True - - - ..\packages\log4net.2.0.5\lib\net45-full\log4net.dll - True - - - ..\packages\Newtonsoft.Json.8.0.2\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - - -<<<<<<< HEAD - -======= - - - ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -<<<<<<< HEAD -======= - - - - - - - - ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - - - - - - - - - - {fc331a95-fca4-4764-8004-0884665dd01f} - OneTrueError.Api - - - {5ef42a74-9323-49fa-a1f6-974d6de77202} - OneTrueError.App - - -<<<<<<< HEAD - - - -======= - ->>>>>>> 1f85023bc3bc0d14087f34d7c3c2906831d91915 - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Properties/AssemblyInfo.cs b/src/Server/OneTrueError.SqlServer/Properties/AssemblyInfo.cs deleted file mode 100644 index 5ad9eca9..00000000 --- a/src/Server/OneTrueError.SqlServer/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. - -[assembly: AssemblyTitle("OneTrueError.SqlServer")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("OneTrueError.SqlServer")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. - -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM - -[assembly: Guid("b967beea-cddd-4a83-a4f2-1c946099ed51")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] - -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Schema/Database.sql b/src/Server/OneTrueError.SqlServer/Schema/Database.sql deleted file mode 100644 index a982928b..00000000 --- a/src/Server/OneTrueError.SqlServer/Schema/Database.sql +++ /dev/null @@ -1,325 +0,0 @@ -IF OBJECT_ID(N'dbo.[Settings]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Settings]( - [Section] [varchar](50) NOT NULL, - [Name] [varchar](50) NOT NULL, - [Value] [varchar](512), - ) ON [PRIMARY] - END - - -IF OBJECT_ID(N'dbo.[Accounts]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Accounts]( - [Id] [int] IDENTITY(1,1) NOT NULL, - [UserName] [varchar](50) NOT NULL, - [HashedPassword] [varchar](512) NOT NULL, - [CreatedAtUtc] [datetime] NOT NULL, - [Email] [varchar](255) NOT NULL, - [Salt] [varchar](512) NOT NULL, - [AccountState] [varchar](20) NOT NULL, - [TrackingId] [varchar](40) NULL, - [LoginAttempts] [int] NOT NULL, - [LastLoginAtUtc] [datetime] NULL, - [ActivationKey] [varchar](50) NULL, - [PromotionCode] [varchar](50) NULL, - [UpdatedAtUtc] [datetime] NULL, - CONSTRAINT [accounts_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) - ) ON [PRIMARY] - END - - IF OBJECT_ID(N'dbo.[InvalidReports]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[InvalidReports]( - [Id] [int] IDENTITY(1,1) NOT NULL, - [AppKey] [varchar](36) NOT NULL, - [Signature] [varchar](36) NOT NULL, - [ReportBody] [ntext] NOT NULL, - [ErrorMessage] [varchar](2000) NOT NULL, - [CreatedAtUtc] [datetime] NOT NULL, - CONSTRAINT [invalidreports_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) - ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] -END - - -IF OBJECT_ID(N'dbo.[Invitations]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Invitations]( - [Id] [int] IDENTITY(1,1) NOT NULL, - [Email] [varchar](2000) NOT NULL, - [InvitationKey] [char](32) NOT NULL, - [CreatedAtUtc] [datetime] NOT NULL, - [InvitedBy] varchar(50) NOT NULL, - [Invitations] varchar(2500) NOT NULL, - - CONSTRAINT [invitations_pkey] PRIMARY KEY CLUSTERED ([Id] ASC) - ) ON [PRIMARY] -END - -IF OBJECT_ID(N'dbo.[Applications]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Applications] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [Name] NVARCHAR (50) NOT NULL, - [AppKey] VARCHAR (36) NOT NULL, - [CreatedById] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [ApplicationType] VARCHAR (40) NOT NULL, - [SharedSecret] VARCHAR (36) NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[CollectionMetadata]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[CollectionMetadata] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [Name] NVARCHAR (50) NOT NULL, - [ApplicationId] INT NOT NULL, - [Properties] NTEXT NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[ErrorOrigins]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ErrorOrigins] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [IpAddress] VARCHAR (20) NOT NULL, - [CountryCode] VARCHAR (5) NULL, - [CountryName] VARCHAR (30) NULL, - [RegionCode] VARCHAR (5) NULL, - [RegionName] VARCHAR (30) NULL, - [City] NVARCHAR (30) NULL, - [ZipCode] VARCHAR (10) NULL, - [Latitude] DECIMAL (9, 6) NOT NULL, - [Longitude] DECIMAL (9, 6) NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[ErrorReportOrigins]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ErrorReportOrigins] ( - [ErrorOriginId] INT NOT NULL, - [IncidentId] INT NOT NULL, - [ReportId] INT NOT NULL, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[ErrorReports]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ErrorReports] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [IncidentId] INT NOT NULL, - [ErrorId] VARCHAR (36) NOT NULL, - [ApplicationId] INT NOT NULL, - [ReportHashCode] VARCHAR (20) NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [SolvedAtUtc] DATETIME NULL, - [Title] NVARCHAR(100) NULL, - [RemoteAddress] VARCHAR (45) NULL, --SEE http://stackoverflow.com/questions/166132/maximum-length-of-the-textual-representation-of-an-ipv6-address - [Exception] NTEXT NOT NULL, - [ContextInfo] NTEXT NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[ErrorReports_IncidentId]', N'I') IS NULL -BEGIN - CREATE NONCLUSTERED INDEX [ErrorReports_IncidentId] - ON [dbo].[ErrorReports]([IncidentId] ASC, [CreatedAtUtc] DESC); -END - -IF OBJECT_ID(N'dbo.[Application_GetWeeklyStats]', N'I') IS NULL -BEGIN - CREATE NONCLUSTERED INDEX [Application_GetWeeklyStats] - ON [dbo].[ErrorReports]([ApplicationId] ASC, [CreatedAtUtc] DESC); -END - -IF OBJECT_ID(N'dbo.[IncidentFeedback]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[IncidentFeedback] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [ApplicationId] INT NULL, - [IncidentId] INT NULL, - [ReportId] INT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [RemoteAddress] VARCHAR (20) NOT NULL, - [Description] NTEXT NOT NULL, - [EmailAddress] NVARCHAR (512) NULL, - [Conversation] NTEXT NOT NULL, - [ConversationLength] INT NOT NULL, - [ErrorReportId] VARCHAR (40) NOT NULL, - [Replied] INT NOT NULL default 0 - ); -END - - -IF OBJECT_ID(N'dbo.[Incidents]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Incidents] ( - [Id] INT IDENTITY (1, 1) NOT NULL, - [ReportHashCode] VARCHAR (20) NOT NULL, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [HashCodeIdentifier] VARCHAR (1024) NOT NULL, - [ReportCount] INT NOT NULL, - [UpdatedAtUtc] DATETIME NULL, - [Description] NTEXT NOT NULL, - [FullName] NVARCHAR (255) NOT NULL, - [Solution] NTEXT NULL, - [IsSolved] BINARY (1) NOT NULL default(0), - [IsSolutionShared] BINARY (1) NOT NULL default(0), - [SolvedAtUtc] DATETIME NULL, - [StackTrace] NTEXT NULL, - [IsReOpened] BIT NOT NULL default(0), - [ReOpenedAtUtc] DATETIME NULL, - [PreviousSolutionAtUtc] DATETIME NULL, - [IgnoreReports] BIT NOT NULL default(0), - [IgnoringReportsSinceUtc] DATETIME NULL, - [IgnoringRequestedBy] NVARCHAR (50) NULL, - [LastSolutionAtUtc] DATETIME NULL - ); -END - -IF OBJECT_ID(N'dbo.[IncidentTags]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[IncidentTags] ( - [id] INT IDENTITY (1, 1) NOT NULL primary key, - [IncidentId] INT NOT NULL, - [TagName] VARCHAR (40) NOT NULL, - [OrderNumber] INT NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[IncidentTags_FromIncident]', N'U') IS NULL -BEGIN - CREATE NONCLUSTERED INDEX [IncidentTags_FromIncident] - ON [dbo].[IncidentTags]([IncidentId] ASC, [OrderNumber] ASC); -END - -IF OBJECT_ID(N'dbo.[ReportContextInfo]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ReportContextInfo] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [IncidentId] INT NOT NULL, - [ReportId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [UpdatedAtUtc] DATETIME NULL, - [Name] NVARCHAR (1024) NULL, - [Value] NVARCHAR (20) NULL, - [LargeValue] NTEXT NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[IncidentContextCollections]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[IncidentContextCollections] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [IncidentId] INT NOT NULL, - [Name] VARCHAR (250) NOT NULL, - [Properties] text NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[IncidentContextCollections_IncidentId]', N'U') IS NULL -BEGIN - CREATE NONCLUSTERED INDEX [IncidentContextCollections_IncidentId] - ON [dbo].[IncidentContextCollections]([IncidentId] ASC); -END - - -IF OBJECT_ID(N'dbo.[Triggers]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[Triggers] ( - [Id] INT IDENTITY (1, 1) NOT NULL primary key, - [Name] NVARCHAR (50) NOT NULL, - [Description] NVARCHAR (512) NOT NULL, - [ApplicationId] INT NOT NULL, - [Rules] NTEXT NOT NULL, - [Actions] NTEXT NOT NULL, - [LastTriggerAction] NVARCHAR (50) NOT NULL, - [RunForNewIncidents] BIT NOT NULL, - [RunForExistingIncidents] BIT NOT NULL, - [RunForReOpenedIncidents] BIT NOT NULL - ); -END - - -IF OBJECT_ID(N'dbo.[UserNotificationSettings]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[UserNotificationSettings] ( - [AccountId] INT NOT NULL, - [ApplicationId] INT NOT NULL, - [NewIncident] VARCHAR (20) NOT NULL default 'Disabled', - [NewReport] VARCHAR (20) NOT NULL default 'Disabled', - [ReOpenedIncident] VARCHAR (20) NOT NULL default 'Disabled', - [WeeklySummary] VARCHAR (20) NOT NULL default 'Disabled', - [ApplicationSpike] VARCHAR (20) NOT NULL default 'Disabled', - [UserFeedback] VARCHAR (20) NOT NULL default 'Disabled' - ); -END -ALTER TABLE [UserNotificationSettings] - ADD CONSTRAINT pk_UserNotificationSettings PRIMARY KEY (AccountId, ApplicationId); - -CREATE TABLE dbo.Users -( - AccountId INT NOT NULL primary key, - EmailAddress varchar(255) not null, - FirstName varchar(100), - LastName varchar(100), - UserName varchar(100), - MobileNumber varchar(100) - ); - - -IF OBJECT_ID(N'dbo.[ApplicationMembers]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ApplicationMembers] ( - [AccountId] INT NULL foreign key references Accounts (Id), - [ApplicationId] INT NOT NULL foreign key references Applications (Id), - [EmailAddress] nvarchar(255) not null, - [AddedAtUtc] DATETIME NOT NULL, - [AddedByName] VARCHAR (50) NOT NULL, - [Roles] VARCHAR (255) NOT NULL - ); -END - -IF OBJECT_ID(N'dbo.[QueueEvents]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[QueueEvents] ( - [Id] INT identity not NULL primary key, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, - [Body] text NOT NULL, - ); -END - -IF OBJECT_ID(N'dbo.[QueueReports]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[QueueReports] ( - [Id] INT identity not NULL primary key, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, - [Body] text NOT NULL, - - ); -END - - -IF OBJECT_ID(N'dbo.[QueueFeedback]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[QueueFeedback] ( - [Id] INT identity not NULL primary key, - [ApplicationId] INT NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [AssemblyQualifiedTypeName] VARCHAR (255) NOT NULL, - [Body] text NOT NULL, - - ); -END diff --git a/src/Server/OneTrueError.SqlServer/Schema/Update.v2.sql b/src/Server/OneTrueError.SqlServer/Schema/Update.v2.sql deleted file mode 100644 index 9283d3d6..00000000 --- a/src/Server/OneTrueError.SqlServer/Schema/Update.v2.sql +++ /dev/null @@ -1,27 +0,0 @@ -IF OBJECT_ID(N'dbo.[DatabaseSchema]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[DatabaseSchema] ( - [Version] int not null default 1 - ); - INSERT INTO DatabaseSchema VALUES(1); -END - -IF OBJECT_ID(N'dbo.[ApiKeys]', N'U') IS NULL -BEGIN - CREATE TABLE [dbo].[ApiKeys] ( - [Id] INT identity not NULL primary key, - [ApplicationName] varchar(40) NOT NULL, - [CreatedAtUtc] DATETIME NOT NULL, - [CreatedById] int NOT NULL, - [GeneratedKey] varchar(36) NOT NULL, - [SharedSecret] varchar(36) NOT NULL - ); - CREATE TABLE [dbo].[ApiKeyApplications] ( - [ApiKeyId] INT not NULL, - [ApplicationId] INT NOT NULL, - Primary key (ApiKeyId, ApplicationId), - FOREIGN KEY (ApiKeyId) REFERENCES ApiKeys(Id) ON DELETE CASCADE, - FOREIGN KEY (ApplicationId) REFERENCES Applications(Id) ON DELETE NO ACTION - ); - update DatabaseSchema SET Version = 2; -END diff --git a/src/Server/OneTrueError.SqlServer/Schema/Update.v3.sql b/src/Server/OneTrueError.SqlServer/Schema/Update.v3.sql deleted file mode 100644 index 8f1abda7..00000000 --- a/src/Server/OneTrueError.SqlServer/Schema/Update.v3.sql +++ /dev/null @@ -1,43 +0,0 @@ -ALTER TABLE Incidents ADD PRIMARY KEY (Id); -ALTER TABLE ErrorReportOrigins WITH CHECK ADD CONSTRAINT FK_ErrorReportOrigins_Reports FOREIGN KEY (ReportId) REFERENCES ErrorReports (Id) ON DELETE CASCADE; -ALTER TABLE ErrorReports WITH CHECK ADD CONSTRAINT FK_ErrorReports_Incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE CollectionMetadata WITH CHECK ADD CONSTRAINT FK_COLME_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; -ALTER TABLE IncidentFeedback WITH CHECK ADD CONSTRAINT FK_IncidentFeedback_incidents FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE Incidents WITH CHECK ADD CONSTRAINT FK_Incidents_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; -ALTER TABLE IncidentTags WITH CHECK ADD CONSTRAINT FK_IncidentTags_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE ReportContextInfo WITH CHECK ADD CONSTRAINT FK_ReportContextInfo_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE IncidentContextCollections WITH CHECK ADD CONSTRAINT FK_ICC_incidentId FOREIGN KEY (IncidentId) REFERENCES Incidents (Id) ON DELETE CASCADE; -ALTER TABLE [UserNotificationSettings] WITH CHECK ADD CONSTRAINT FK_UNS_accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE; -ALTER TABLE [Triggers] WITH CHECK ADD CONSTRAINT FK_Triggers_applicationId FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; - -DECLARE @ConstraintName nvarchar(200) -SELECT @ConstraintName = KCU.CONSTRAINT_NAME -FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC -INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU - ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG - AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA - AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME -WHERE - KCU.TABLE_NAME = 'ApplicationMembers' AND - KCU.COLUMN_NAME = 'AccountId' -IF @ConstraintName IS NOT NULL -BEGIN - EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName) -END; -SELECT @ConstraintName = KCU.CONSTRAINT_NAME -FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC -INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU - ON KCU.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG - AND KCU.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA - AND KCU.CONSTRAINT_NAME = RC.CONSTRAINT_NAME -WHERE - KCU.TABLE_NAME = 'ApplicationMembers' AND - KCU.COLUMN_NAME = 'ApplicationId' -IF @ConstraintName IS NOT NULL -BEGIN - EXEC('ALTER TABLE ApplicationMembers DROP CONSTRAINT ' + @ConstraintName) -END -ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Accounts FOREIGN KEY (AccountId) REFERENCES Accounts (Id) ON DELETE CASCADE; -ALTER TABLE ApplicationMembers WITH CHECK ADD CONSTRAINT FK_AppMemb_Applications FOREIGN KEY (ApplicationId) REFERENCES Applications (Id) ON DELETE CASCADE; - -UPDATE DatabaseSchema SET Version = 3; diff --git a/src/Server/OneTrueError.SqlServer/Schema/Update.v4.sql b/src/Server/OneTrueError.SqlServer/Schema/Update.v4.sql deleted file mode 100644 index 710a4c47..00000000 --- a/src/Server/OneTrueError.SqlServer/Schema/Update.v4.sql +++ /dev/null @@ -1,8 +0,0 @@ ---version 1.0 (part A) of OneTrueError - -ALTER TABLE Accounts ADD IsSysAdmin bit not null default 0; -alter table ApplicationMembers add Id int identity not null primary key; -ALTER TABLE ApplicationMembers ALTER COLUMN [EmailAddress] nvarchar(255) null; - - -UPDATE DatabaseSchema SET Version = 4; diff --git a/src/Server/OneTrueError.SqlServer/Schema/Update.v5.sql b/src/Server/OneTrueError.SqlServer/Schema/Update.v5.sql deleted file mode 100644 index 2818128a..00000000 --- a/src/Server/OneTrueError.SqlServer/Schema/Update.v5.sql +++ /dev/null @@ -1,5 +0,0 @@ ---version 1.0 of OneTrueError ---a split was required due to updating created column -UPDATE Accounts SET IsSysAdmin = 1 WHERE Id = (SELECT TOP 1 Id FROM ACCOUNTS ORDER BY Id); - -UPDATE DatabaseSchema SET Version = 5; diff --git a/src/Server/OneTrueError.SqlServer/SchemaManager.cs b/src/Server/OneTrueError.SqlServer/SchemaManager.cs deleted file mode 100644 index 9bce00ca..00000000 --- a/src/Server/OneTrueError.SqlServer/SchemaManager.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using System.Data; -using System.Data.SqlClient; -using System.IO; -using System.Linq; -using System.Reflection; - -namespace OneTrueError.SqlServer -{ - public class SchemaManager - { - private readonly Func _connectionFactory; - - public SchemaManager(Func connectionFactory) - { - _connectionFactory = connectionFactory; - } - - /// - /// Check if the current DB schema is out of date compared to the embedded schema resources. - /// - public bool CanSchemaBeUpgraded() - { - var version = GetCurrentSchemaVersion(); - var embeddedSchema = GetLatestSchemaVersion(); - return embeddedSchema > version; - } - - - public void CreateInitialStructure() - { - using (var con = _connectionFactory()) - { - var resourceName = "OneTrueError.SqlServer.Schema.Database.sql"; - var res = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); - var sql = new StreamReader(res).ReadToEnd(); - using (var transaction = con.BeginTransaction()) - using (var cmd = con.CreateCommand()) - { - cmd.Transaction = transaction; - cmd.CommandText = sql; - cmd.ExecuteNonQuery(); - transaction.Commit(); - } - } - } - - public int GetCurrentSchemaVersion() - { - var version = 0; - using (var con = _connectionFactory()) - { - using (var cmd = con.CreateCommand()) - { - try - { - var sql = "SELECT Version FROM DatabaseSchema"; - cmd.CommandText = sql; - var result = cmd.ExecuteScalar(); - version = (int) result; - } - catch (SqlException ex) - { - //invalid object name - if (ex.Number == 208) - return -1; - - throw; - } - } - } - return version; - } - - public int GetLatestSchemaVersion() - { - var highestVersion = 0; - var ns = "OneTrueError.SqlServer.Schema"; - var names = - Assembly.GetExecutingAssembly() - .GetManifestResourceNames() - .Where(x => x.StartsWith(ns) && x.Contains(".Update.")); - foreach (var name in names) - { - var pos = name.IndexOf("Update") + 8; //2 extra for ".v" - var endPos = name.IndexOf(".", pos); - var versionStr = name.Substring(pos, endPos - pos); - var version = int.Parse(versionStr); - if (version > highestVersion) - highestVersion = version; - } - return highestVersion; - } - - public void UpgradeDatabaseSchema() - { - var latestSchemaVersion = GetLatestSchemaVersion(); - var currentSchema = GetCurrentSchemaVersion(); - if (currentSchema < 1) - currentSchema = 1; - - for (var version = currentSchema + 1; version <= latestSchemaVersion; version++) - { - var schema = GetSchema(version); - using (var con = _connectionFactory()) - { - using (var transaction = con.BeginTransaction()) - using (var cmd = con.CreateCommand()) - { - cmd.Transaction = transaction; - cmd.CommandText = schema; - cmd.ExecuteNonQuery(); - transaction.Commit(); - } - } - } - } - - private string GetSchema(int version) - { - var resourceName = "OneTrueError.SqlServer.Schema.Update.v" + version + ".sql"; - var res = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); - if (res == null) - throw new InvalidOperationException("Failed to find schema " + resourceName); - - return new StreamReader(res).ReadToEnd(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/SqlServerTools.cs b/src/Server/OneTrueError.SqlServer/SqlServerTools.cs deleted file mode 100644 index 2b993f0f..00000000 --- a/src/Server/OneTrueError.SqlServer/SqlServerTools.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Configuration; -using System.Data; -using System.Data.Common; -using System.Data.SqlClient; -using System.Diagnostics.CodeAnalysis; -using OneTrueError.Infrastructure; - -namespace OneTrueError.SqlServer -{ - /// - /// MS Sql Server specific implementation of the database tools. - /// - public class SqlServerTools : ISetupDatabaseTools - { - private readonly SchemaManager _schemaManager; - - public SqlServerTools() - { - _schemaManager = new SchemaManager(OpenConnection); - } - - internal static string DbName { get; set; } - - private bool IsConnectionConfigured - { - get - { - return ConfigurationManager.ConnectionStrings["Db"] != null && - !string.IsNullOrEmpty(ConfigurationManager.ConnectionStrings["Db"].ConnectionString); - } - } - - /// - /// Checks if the tables exists and are for the current DB schema. - /// - public bool GotUpToDateTables() - { - if (!IsConnectionConfigured) - return false; - - using (var con = OpenConnection()) - { - using (var cmd = con.CreateCommand()) - { - cmd.CommandText = "SELECT OBJECT_ID(N'dbo.[Accounts]', N'U')"; - var result = cmd.ExecuteScalar(); - - //null for SQL Express and DbNull for SQL Server - return result != null && !(result is DBNull); - } - } - } - - /// - /// Check if the current DB schema is out of date compared to the embedded schema resources. - /// - public bool CanSchemaBeUpgraded() - { - return _schemaManager.CanSchemaBeUpgraded(); - } - - /// - /// Update DB schema to latest version. - /// - public void UpgradeDatabaseSchema() - { - _schemaManager.UpgradeDatabaseSchema(); - } - - public void CheckConnectionString(string connectionString) - { - var pos = connectionString.IndexOf("Connect Timeout="); - if (pos != -1) - { - pos += "Connect Timeout=".Length; - var endPos = connectionString.IndexOf(";", pos); - if (endPos == -1) - connectionString = connectionString.Substring(0, pos) + "1"; - else - connectionString = connectionString.Substring(0, pos) + "1" + connectionString.Substring(endPos); - } - - var con = new SqlConnection(connectionString); - con.Open(); - } - - [SuppressMessage("Microsoft.Security", "CA2100:Review SQL queries for security vulnerabilities", - Justification = "Installation import = control over SQL")] - public void CreateTables() - { - _schemaManager.CreateInitialStructure(); - } - - IDbConnection ISetupDatabaseTools.OpenConnection() - { - return OpenConnection(); - } - - public static IDbConnection OpenConnection(string connectionString) - { - var con = new SqlConnection(connectionString); - con.Open(); - if (DbName != null) - con.ChangeDatabase(DbName); - - return con; - } - - public static IDbConnection OpenConnection() - { - var conStr = ConfigurationManager.ConnectionStrings["Db"]; - var provider = DbProviderFactories.GetFactory(conStr.ProviderName); - var connection = provider.CreateConnection(); - connection.ConnectionString = conStr.ConnectionString; - connection.Open(); - return connection; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Tools/ICustomerIdPrincipal.cs b/src/Server/OneTrueError.SqlServer/Tools/ICustomerIdPrincipal.cs deleted file mode 100644 index 291bc97d..00000000 --- a/src/Server/OneTrueError.SqlServer/Tools/ICustomerIdPrincipal.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Security.Principal; - -namespace OneTrueError.SqlServer.Tools -{ - public interface ICustomerIdPrincipal : IPrincipal - { - int CustomerId { get; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs b/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs deleted file mode 100644 index 14d9b1bf..00000000 --- a/src/Server/OneTrueError.SqlServer/Web/Feedback/Queries/GetOverviewFeedbackResultItemMapper.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Griffin.Data.Mapper; -using OneTrueError.Api.Web.Feedback.Queries; - -namespace OneTrueError.SqlServer.Web.Feedback.Queries -{ - public class GetOverviewFeedbackResultItemMapper : EntityMapper - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/Web/Overview/GetOverviewHandler.cs b/src/Server/OneTrueError.SqlServer/Web/Overview/GetOverviewHandler.cs deleted file mode 100644 index 6603d61f..00000000 --- a/src/Server/OneTrueError.SqlServer/Web/Overview/GetOverviewHandler.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.Container; -using Griffin.Data; -using OneTrueError.Api.Web.Overview.Queries; -using OneTrueError.Infrastructure.Security; - -namespace OneTrueError.SqlServer.Web.Overview -{ - [Component] - internal class GetOverviewHandler : IQueryHandler - { - private readonly IAdoNetUnitOfWork _unitOfWork; - private string _appIds; - - public GetOverviewHandler(IAdoNetUnitOfWork unitOfWork) - { - _unitOfWork = unitOfWork; - } - - private DateTime StartDateForHours - { - get - { - //since we want to 22 if time is 21:30 - return DateTime.Today.AddHours(DateTime.Now.Hour).AddHours(-22); - } - } - - private string ApplicationIds - { - get - { - if (_appIds != null) - return _appIds; - - var appIds = ClaimsPrincipal.Current - .FindAll(x => x.Type == OneTrueClaims.Application) - .Select(x => int.Parse(x.Value).ToString()) - .ToList(); - _appIds = string.Join(",", appIds); - return _appIds; - } - } - - public async Task ExecuteAsync(GetOverview query) - { - if (query.NumberOfDays == 0) - query.NumberOfDays = 30; - var labels = CreateTimeLabels(query); - - if (!ClaimsPrincipal.Current.FindAll(x => x.Type == OneTrueClaims.Application).Any()) - { - return new GetOverviewResult() - { - StatSummary = new OverviewStatSummary(), - IncidentsPerApplication = new GetOverviewApplicationResult[0], - TimeAxisLabels = labels - }; - } - - if (query.NumberOfDays == 1) - return await GetTodaysOverviewAsync(query); - - var apps = new Dictionary(); - var startDate = DateTime.Today.AddDays(-query.NumberOfDays); - var result = new GetOverviewResult(); - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = $@"select Applications.Id, Applications.Name, cte.Date, cte.Count -FROM -( - select Incidents.ApplicationId , cast(Incidents.CreatedAtUtc as date) as Date, count(Incidents.Id) as Count - from Incidents - where Incidents.CreatedAtUtc >= @minDate - AND Incidents.CreatedAtUtc <= GetUtcDate() - AND Incidents.ApplicationId in ({ApplicationIds}) - group by Incidents.ApplicationId, cast(Incidents.CreatedAtUtc as date) -) cte -right join applications on (applicationid=applications.id) - -;"; - - - cmd.AddParameter("minDate", startDate); - using (var reader = await cmd.ExecuteReaderAsync()) - { - while (await reader.ReadAsync()) - { - var appId = reader.GetInt32(0); - GetOverviewApplicationResult app; - if (!apps.TryGetValue(appId, out app)) - { - app = new GetOverviewApplicationResult(reader.GetString(1), startDate, - query.NumberOfDays + 1); //+1 for today - apps[appId] = app; - } - //no stats at all for this app - if (reader[2] is DBNull) - { - var startDate2 = DateTime.Today.AddDays(-query.NumberOfDays + 1); - for (var i = 0; i < query.NumberOfDays; i++) - { - app.AddValue(startDate2.AddDays(i), 0); - } - } - else - app.AddValue(reader.GetDateTime(2), reader.GetInt32(3)); - } - - result.TimeAxisLabels = labels; - result.IncidentsPerApplication = apps.Values.ToArray(); - } - } - - await GetStatSummary(query, result); - - - return result; - } - - private static string[] CreateTimeLabels(GetOverview query) - { - var startDate = DateTime.Today.AddDays(-query.NumberOfDays); - var labels = new string[query.NumberOfDays + 1]; //+1 for today - for (var i = 0; i <= query.NumberOfDays; i++) - { - labels[i] = startDate.AddDays(i).ToShortDateString(); - } - return labels; - } - - private async Task GetStatSummary(GetOverview query, GetOverviewResult result) - { - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = string.Format(@"select count(id) from incidents -where CreatedAtUtc >= @minDate -AND CreatedAtUtc <= GetUtcDate() -AND Incidents.ApplicationId IN ({0}) -AND Incidents.IgnoreReports = 0 -AND Incidents.IsSolved = 0; - -select count(id) from errorreports -where CreatedAtUtc >= @minDate -AND ApplicationId IN ({0}) - -select count(distinct emailaddress) from IncidentFeedback -where CreatedAtUtc >= @minDate -AND CreatedAtUtc <= GetUtcDate() -AND ApplicationId IN ({0}) -AND emailaddress is not null -AND DATALENGTH(emailaddress) > 0; - -select count(*) from IncidentFeedback -where CreatedAtUtc >= @minDate -AND CreatedAtUtc <= GetUtcDate() -AND ApplicationId IN ({0}) -AND Description is not null -AND DATALENGTH(Description) > 0;", ApplicationIds); - - var minDate = query.NumberOfDays == 1 - ? StartDateForHours - : DateTime.Today.AddDays(-query.NumberOfDays); - cmd.AddParameter("minDate", minDate); - - using (var reader = await cmd.ExecuteReaderAsync()) - { - if (!await reader.ReadAsync()) - { - throw new InvalidOperationException("Expected to be able to read."); - } - - var data = new OverviewStatSummary(); - data.Incidents = reader.GetInt32(0); - await reader.NextResultAsync(); - await reader.ReadAsync(); - data.Reports = reader.GetInt32(0); - await reader.NextResultAsync(); - await reader.ReadAsync(); - data.Followers = reader.GetInt32(0); - await reader.NextResultAsync(); - await reader.ReadAsync(); - data.UserFeedback = reader.GetInt32(0); - result.StatSummary = data; - } - } - } - - private async Task GetTodaysOverviewAsync(GetOverview query) - { - var result = new GetOverviewResult - { - TimeAxisLabels = new string[24] - }; - var startDate = StartDateForHours; - var apps = new Dictionary(); - for (var i = 0; i < 24; i++) - { - result.TimeAxisLabels[i] = startDate.AddHours(i).ToString("HH:mm"); - } - - using (var cmd = _unitOfWork.CreateDbCommand()) - { - cmd.CommandText = string.Format(@"select Applications.Id, Applications.Name, cte.Date, cte.Count -FROM -( - select Incidents.ApplicationId , DATEPART(HOUR, Incidents.CreatedAtUtc) as Date, count(Incidents.Id) as Count - from Incidents - where Incidents.CreatedAtUtc >= @minDate - AND CreatedAtUtc <= GetUtcDate() - AND Incidents.ApplicationId IN ({0}) - group by Incidents.ApplicationId, DATEPART(HOUR, Incidents.CreatedAtUtc) -) cte -right join applications on (applicationid=applications.id)", ApplicationIds); - - - cmd.AddParameter("minDate", startDate); - using (var reader = await cmd.ExecuteReaderAsync()) - { - while (await reader.ReadAsync()) - { - var appId = reader.GetInt32(0); - GetOverviewApplicationResult app; - if (!apps.TryGetValue(appId, out app)) - { - app = new GetOverviewApplicationResult(reader.GetString(1), startDate, 1); - apps[appId] = app; - } - - if (reader[2] is DBNull) - { - for (var i = 0; i < 24; i++) - { - app.AddValue(startDate.AddHours(i), 0); - } - } - else - { - var hour = reader.GetInt32(2); - app.AddValue( - hour < DateTime.Now.AddHours(1).Hour //since we want 22:00 if time is 21:30 - ? DateTime.Today.AddHours(hour) - : DateTime.Today.AddDays(-1).AddHours(hour), reader.GetInt32(3)); - } - } - - result.IncidentsPerApplication = apps.Values.ToArray(); - } - } - - await GetStatSummary(query, result); - - return result; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/app.config b/src/Server/OneTrueError.SqlServer/app.config deleted file mode 100644 index 936401de..00000000 --- a/src/Server/OneTrueError.SqlServer/app.config +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.SqlServer/packages.config b/src/Server/OneTrueError.SqlServer/packages.config deleted file mode 100644 index 962bc8fc..00000000 --- a/src/Server/OneTrueError.SqlServer/packages.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/BundleConfig.cs b/src/Server/OneTrueError.Web/App_Start/BundleConfig.cs deleted file mode 100644 index 85b28f87..00000000 --- a/src/Server/OneTrueError.Web/App_Start/BundleConfig.cs +++ /dev/null @@ -1,52 +0,0 @@ -using System.Web.Optimization; - -namespace OneTrueError.Web -{ - public class BundleConfig - { - // For more information on bundling, visit http://go.microsoft.com/fwlink/?LinkId=301862 - public static void RegisterBundles(BundleCollection bundles) - { - bundles.Add(new ScriptBundle("~/bundles/jquery").Include( - "~/Scripts/jquery-{version}.js", - "~/Scripts/humane.js", - "~/Scripts/application.js", - "~/Scripts/prism.js", - "~/Scripts/marked.min.js", - "~/Scripts/Base64.js", - "~/Scripts/transparency.min.js") - ); - - bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include( - "~/Scripts/jquery.validate*")); - - - // Use the development version of Modernizr to develop with and learn from. Then, when you're - // ready for production, use the build tool at http://modernizr.com to pick only the tests you need. - bundles.Add(new ScriptBundle("~/bundles/modernizr").Include( - "~/Scripts/modernizr-*")); - - bundles.Add(new ScriptBundle("~/bundles/bootstrap").Include( - "~/Scripts/bootstrap.js", - "~/Scripts/respond.js")); - - bundles.Add(new ScriptBundle("~/bundles/app") - .Include( - "~/Scripts/utils.js", - "~/Scripts/CqsClient.js", - "~/Scripts/Griffin.Yo.js", - "~/Scripts/Griffin.Net.js", - "~/Scripts/Griffin.WebApp.js", - "~/ViewModels/ChartViewModel.js", - "~/Scripts/Models/AllModels.js") - .IncludeDirectory("~/app/", "*.js", true) - ); - - - bundles.Add(new StyleBundle("~/Content/css").Include( - "~/Content/ote_bootstrap.min.css", - "~/Content/humane.flatty.css", - "~/Content/site.css")); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/CompositionRoot.cs b/src/Server/OneTrueError.Web/App_Start/CompositionRoot.cs deleted file mode 100644 index af1b8c74..00000000 --- a/src/Server/OneTrueError.Web/App_Start/CompositionRoot.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Configuration; -using System.Data.SqlClient; -using System.Reflection; -using System.Web.Http; -using System.Web.Mvc; -using Griffin.Container; -using Griffin.Container.Mvc5; -using Griffin.Data; -using OneTrueError.App.Core.Accounts.Requests; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.ReportAnalyzer.Scanners; -using OneTrueError.SqlServer.Core.Users; -using OneTrueError.Web.IoC; - -namespace OneTrueError.Web -{ - public class CompositionRoot - { - public static IContainer Container; - - public void Build(Action action) - { - var builder = new ContainerRegistrar(); - builder.RegisterComponents(Lifetime.Scoped, Assembly.GetExecutingAssembly()); - builder.RegisterService(CreateConnection, Lifetime.Scoped); - builder.RegisterService(CreateTaskInvoker, Lifetime.Singleton); - action(builder); - - builder.RegisterComponents(Lifetime.Scoped, typeof(ValidateNewLoginHandler).Assembly); - builder.RegisterComponents(Lifetime.Scoped, typeof(UserRepository).Assembly); - builder.RegisterComponents(Lifetime.Scoped, typeof(ScanForNewErrorReports).Assembly); - builder.RegisterComponents(Lifetime.Scoped, typeof(QueueProvider).Assembly); - - builder.RegisterService(x => Container, Lifetime.Singleton); - builder.RegisterService(x => x); - - builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); - builder.RegisterControllers(Assembly.GetExecutingAssembly()); - - var ioc = builder.Build(); - - DependencyResolver.SetResolver(new GriffinDependencyResolver(ioc)); - GlobalConfiguration.Configuration.DependencyResolver = new GriffinWebApiDependencyResolver2(ioc); - Container = new GriffinContainerAdapter(ioc); - } - - private IAdoNetUnitOfWork CreateConnection(IServiceLocator arg) - { - var conStr = ConfigurationManager.ConnectionStrings["Db"]; - var connection = new SqlConnection(conStr.ConnectionString); - connection.Open(); - return new AdoNetUnitOfWork(connection, true); - } - - private IScopedTaskInvoker CreateTaskInvoker(IServiceLocator arg) - { - var invoker = new ScopedTaskInvoker(Container); - invoker.TaskExecuted += (sender, args) => args.Scope.Resolve().SaveChanges(); - return invoker; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/Cqs/CqsBuilder.cs b/src/Server/OneTrueError.Web/App_Start/Cqs/CqsBuilder.cs deleted file mode 100644 index 527ae1a7..00000000 --- a/src/Server/OneTrueError.Web/App_Start/Cqs/CqsBuilder.cs +++ /dev/null @@ -1,101 +0,0 @@ -using System; -using DotNetCqs; -using Griffin.Container; -using Griffin.Cqs.InversionOfControl; -using Griffin.Data; -using log4net; -using OneTrueError.App.Core.Accounts.Requests; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.ReportAnalyzer.Scanners; -using OneTrueError.SqlServer.Core.Users; - -namespace OneTrueError.Web.Cqs -{ - public class CqsBuilder - { - private static readonly MyEventHandlerRegistry _registry = new MyEventHandlerRegistry(); - private readonly ILog _log = LogManager.GetLogger(typeof(CqsBuilder)); - private readonly IMessageQueueProvider _queueProvider = new QueueProvider(); - private IEventBus _eventBus; - - public EventHandlerRegistry EventHandlerRegistry - { - get { return _registry; } - } - - public static void CloseUnitOfWorks(IContainerScope scope) - { - scope.Resolve().SaveChanges(); - } - - public ICommandBus CreateCommandBus(IContainer container) - { - var iocBus = new IocCommandBus(container); - iocBus.CommandInvoked += OnCommandInvoked; - iocBus.ScopeCreated += OnCommandScope; - return iocBus; - } - - public IEventBus CreateEventBus(IContainer container) - { - // should not happen, but does sometimes. - // seems related to Mvc contra WebApi - // but can't figure out why - if (_eventBus != null) - return _eventBus; - - _registry.ScanAssembly(typeof(ValidateNewLoginHandler).Assembly); - _registry.ScanAssembly(typeof(UserRepository).Assembly); - _registry.ScanAssembly(typeof(ScanForNewErrorReports).Assembly); - - //var inner = new SeparateScopesIocEventBus(container, _registry); - var inner = new IocEventBus(container); - inner.EventPublished += (sender, args) => CloseUnitOfWorks(args.Scope); - //inner.ScopeClosing += (sender, args) => CloseUnitOfWorks(args.Scope); - inner.HandlerFailed += (sender, args) => - { - foreach (var failure in args.Failures) - { - _log.Error(failure.Handler.GetType().FullName + " failed to handle " + args.ApplicationEvent, - failure.Exception); - } - }; - - var bus = new QueuedEventBus(inner, _queueProvider); - _eventBus = bus; - - return bus; - } - - public IQueryBus CreateQueryBus(IContainer container) - { - var bus = new IocQueryBus(container); - bus.QueryExecuted += (sender, args) => { CloseUnitOfWorks(args.Scope); }; - return bus; - } - - public IRequestReplyBus CreateRequestReplyBus(IContainer container) - { - var bus = new IocRequestReplyBus(container); - bus.RequestInvoked += (sender, args) => { CloseUnitOfWorks(args.Scope); }; - return bus; - } - - private void OnCommandInvoked(object sender, CommandInvokedEventArgs e) - { - _log.Debug("Invoked " + e.Command); - try - { - CloseUnitOfWorks(e.Scope); - } - catch (Exception exception) - { - _log.Fatal("failed to commit", exception); - } - } - - private void OnCommandScope(object sender, ScopeCreatedEventArgs e) - { - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/Cqs/JsonFormatter.cs b/src/Server/OneTrueError.Web/App_Start/Cqs/JsonFormatter.cs deleted file mode 100644 index 21691267..00000000 --- a/src/Server/OneTrueError.Web/App_Start/Cqs/JsonFormatter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.IO; -using System.Messaging; -using System.Text; -using Newtonsoft.Json; - -namespace OneTrueError.Web.Cqs -{ - public class JsonFormatter : IMessageFormatter - { - public object Clone() - { - return this; - } - - public bool CanRead(Message message) - { - return true; - } - - public object Read(Message message) - { - var extension = Metadata.Parse(message.Extension); - var type = extension.CreateType(); - var json = new StreamReader(message.BodyStream).ReadToEnd(); - return JsonConvert.DeserializeObject(json, type); - } - - public void Write(Message message, object obj) - { - var extension = new Metadata(obj.GetType()).Serialize(); - message.Extension = extension; - var json = JsonConvert.SerializeObject(obj); - var buf = Encoding.UTF8.GetBytes(json); - message.BodyStream = new MemoryStream(buf); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/Cqs/Metadata.cs b/src/Server/OneTrueError.Web/App_Start/Cqs/Metadata.cs deleted file mode 100644 index 11a91fc4..00000000 --- a/src/Server/OneTrueError.Web/App_Start/Cqs/Metadata.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.IO; -using System.Runtime.Serialization.Formatters; -using System.Runtime.Serialization.Formatters.Binary; - -namespace OneTrueError.Web.Cqs -{ - [Serializable] - public class Metadata - { - public Metadata(Type type) - { - FullTypeName = type.FullName; - AssemblyName = type.Assembly.GetName().Name; - } - - public Metadata() - { - } - - public string AssemblyName { get; set; } - - public string FullTypeName { get; set; } - - public Type CreateType() - { - return Type.GetType(FullTypeName + ", " + AssemblyName); - } - - public static Metadata Parse(byte[] header) - { - var formatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple, - TypeFormat = FormatterTypeStyle.TypesWhenNeeded - }; - var ms = new MemoryStream(header); - return (Metadata) formatter.Deserialize(ms); - } - - public byte[] Serialize() - { - var formatter = new BinaryFormatter - { - AssemblyFormat = FormatterAssemblyStyle.Simple, - TypeFormat = FormatterTypeStyle.TypesWhenNeeded - }; - var ms = new MemoryStream(); - formatter.Serialize(ms, this); - ms.Position = 0; - var buffer = new byte[ms.Length]; - ms.Read(buffer, 0, buffer.Length); - return buffer; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/Cqs/MyEventHandlerRegistry.cs b/src/Server/OneTrueError.Web/App_Start/Cqs/MyEventHandlerRegistry.cs deleted file mode 100644 index fec883b4..00000000 --- a/src/Server/OneTrueError.Web/App_Start/Cqs/MyEventHandlerRegistry.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using System.Reflection; -using Griffin.Cqs.InversionOfControl; - -namespace OneTrueError.Web.Cqs -{ - /// - /// This is a workaround, I KNOW! Fix the problem! - /// - public class MyEventHandlerRegistry : EventHandlerRegistry - { - private readonly List _scannedAssemblies = new List(); - - public new void ScanAssembly(Assembly assembly) - { - if (_scannedAssemblies.Contains(assembly)) - return; - _scannedAssemblies.Add(assembly); - base.ScanAssembly(assembly); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/Cqs/QueuedEventBus.cs b/src/Server/OneTrueError.Web/App_Start/Cqs/QueuedEventBus.cs deleted file mode 100644 index c6c6b216..00000000 --- a/src/Server/OneTrueError.Web/App_Start/Cqs/QueuedEventBus.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; -using DotNetCqs; -using Griffin.ApplicationServices; -using log4net; -using Newtonsoft.Json; -using OneTrueError.Infrastructure.Queueing; - -namespace OneTrueError.Web.Cqs -{ - public class QueuedEventBus : ApplicationServiceThread, IEventBus, IDisposable - { - private readonly ILog _logger = LogManager.GetLogger(typeof(QueuedEventBus)); - private readonly IMessageQueue _queue; - private readonly IEventBus _writeBus; - - public QueuedEventBus(IEventBus writeBus, IMessageQueueProvider queueProvider) - { - _queue = queueProvider.Open("EventQueue"); - _writeBus = writeBus; - } - - public void Dispose() - { - Dispose(true); - } - - public Task PublishAsync(TApplicationEvent e) - where TApplicationEvent : ApplicationEvent - { - try - { - _logger.Debug("Enqueueing: " + e.GetType().Name + " " + JsonConvert.SerializeObject(e)); - _queue.Write(0, e); - } - catch (Exception ex) - { - _logger.Error("Failed to publish " + JsonConvert.SerializeObject(e), ex); - throw; - } - - return Task.FromResult(null); - } - - protected virtual void Dispose(bool isDisposing) - { - //TODO: Dispose queue - } - - protected override void Run(WaitHandle shutdownHandle) - { - _logger.Debug("Starting up event queue..."); - while (!shutdownHandle.WaitOne(0)) - { - object msg = null; - try - { - msg = _queue.Receive(); - } - catch (Exception ex) - { - _logger.Error("Failed to receive.", ex); - if (shutdownHandle.WaitOne(10000)) - break; - continue; - } - - if (msg == null) - { - if (shutdownHandle.WaitOne(1000)) - break; - continue; - } - - try - { - ExecuteMessage(msg); - } - catch (Exception ex) - { - _logger.Error("Processing '" + JsonConvert.SerializeObject(msg) + "' failed.", ex); - } - } - } - - private void ExecuteMessage(object message) - { - _logger.Debug("PUBLISHING " + message.GetType().Name + " " + JsonConvert.SerializeObject(message)); - var method = typeof(IEventBus).GetMethod("PublishAsync"); - var mi = method.MakeGenericMethod(message.GetType()); - var task = (Task) mi.Invoke(_writeBus, new[] {message}); - task.Wait(); - _logger.Debug("Done PUBLISHING " + message.GetType().Name); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/FilterConfig.cs b/src/Server/OneTrueError.Web/App_Start/FilterConfig.cs deleted file mode 100644 index 14746453..00000000 --- a/src/Server/OneTrueError.Web/App_Start/FilterConfig.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System.Web.Mvc; - -namespace OneTrueError.Web -{ - public class FilterConfig - { - public static void RegisterGlobalFilters(GlobalFilterCollection filters) - { - filters.Add(new HandleErrorAttribute()); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/GriffinWebApiChildScope.cs b/src/Server/OneTrueError.Web/App_Start/GriffinWebApiChildScope.cs deleted file mode 100644 index ad5466b0..00000000 --- a/src/Server/OneTrueError.Web/App_Start/GriffinWebApiChildScope.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Http.Dependencies; -using Griffin.Container; - -namespace OneTrueError.Web -{ - public class GriffinWebApiChildScope : IDependencyScope - { - private readonly IChildContainer _childContainer; - - public GriffinWebApiChildScope(IChildContainer childContainer) - { - _childContainer = childContainer; - } - - public void Dispose() - { - _childContainer.Dispose(); - } - - public object GetService(Type serviceType) - { - return GetServices(serviceType).FirstOrDefault(); - } - - public IEnumerable GetServices(Type serviceType) - { - return _childContainer.ResolveAll(serviceType); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/GriffinWebApiDependencyResolver2.cs b/src/Server/OneTrueError.Web/App_Start/GriffinWebApiDependencyResolver2.cs deleted file mode 100644 index 11ef693a..00000000 --- a/src/Server/OneTrueError.Web/App_Start/GriffinWebApiDependencyResolver2.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Http.Dependencies; -using Griffin.Container; -using OneTrueError.GlobalCore.App.Setup; - -namespace OneTrueError.Web -{ - public class GriffinWebApiDependencyResolver2 : IDependencyResolver - { - private readonly Container _ioc; - - public GriffinWebApiDependencyResolver2(Container ioc) - { - _ioc = ioc; - } - - public void Dispose() - { - - } - - public object GetService(Type serviceType) - { - return GetServices(serviceType).FirstOrDefault(); - } - - public IEnumerable GetServices(Type serviceType) - { - return _ioc.ResolveAll(serviceType); - } - - public IDependencyScope BeginScope() - { - return new GriffinWebApiChildScope(_ioc.CreateChildContainer()); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/IoC/GriffinContainerAdapter.cs b/src/Server/OneTrueError.Web/App_Start/IoC/GriffinContainerAdapter.cs deleted file mode 100644 index 3930c13e..00000000 --- a/src/Server/OneTrueError.Web/App_Start/IoC/GriffinContainerAdapter.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Griffin.Container; -using log4net; - -namespace OneTrueError.Web.IoC -{ - public class GriffinContainerAdapter : IContainer - { - private readonly IParentContainer _container; - private ILog _logger = LogManager.GetLogger(typeof(GriffinContainerAdapter)); - - public GriffinContainerAdapter(IParentContainer container) - { - _container = container; - } - - public TService Resolve() - { - return (TService) _container.Resolve(typeof(TService)); - } - - public object Resolve(Type service) - { - return _container.Resolve(service); - } - - public IEnumerable ResolveAll() - { - return _container.ResolveAll(typeof(TService)).Cast(); - } - - public IEnumerable ResolveAll(Type service) - { - return _container.ResolveAll(service); - } - - public IContainerScope CreateScope() - { - return new GriffinContainerScopeAdapter(_container.CreateChildContainer()); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/IoC/GriffinContainerScopeAdapter.cs b/src/Server/OneTrueError.Web/App_Start/IoC/GriffinContainerScopeAdapter.cs deleted file mode 100644 index bfa462bb..00000000 --- a/src/Server/OneTrueError.Web/App_Start/IoC/GriffinContainerScopeAdapter.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Griffin.Container; -using log4net; - -namespace OneTrueError.Web.IoC -{ - public sealed class GriffinContainerScopeAdapter : IContainerScope - { - private static int CurrentScopes; - private readonly IChildContainer _container; - private bool _disposed; - private ILog _logger = LogManager.GetLogger(typeof(GriffinContainerScopeAdapter)); - - public GriffinContainerScopeAdapter(IChildContainer container) - { - _container = container; - Interlocked.Increment(ref CurrentScopes); - } - - public void Dispose() - { - if (_disposed) - return; - _disposed = true; - - Interlocked.Decrement(ref CurrentScopes); - _container.Dispose(); - } - - public TService Resolve() - { - return (TService) _container.Resolve(typeof(TService)); - } - - public object Resolve(Type service) - { - return _container.Resolve(service); - } - - public IEnumerable ResolveAll() - { - return _container.ResolveAll(typeof(TService)).Cast(); - } - - public IEnumerable ResolveAll(Type service) - { - return _container.ResolveAll(service); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/IoC/GriffinWebApiChildScope.cs b/src/Server/OneTrueError.Web/App_Start/IoC/GriffinWebApiChildScope.cs deleted file mode 100644 index c6917b08..00000000 --- a/src/Server/OneTrueError.Web/App_Start/IoC/GriffinWebApiChildScope.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Http.Dependencies; -using Griffin.Container; - -namespace OneTrueError.Web.IoC -{ - public sealed class GriffinWebApiChildScope : IDependencyScope - { - private readonly IChildContainer _childContainer; - - public GriffinWebApiChildScope(IChildContainer childContainer) - { - _childContainer = childContainer; - } - - public void Dispose() - { - _childContainer.Dispose(); - } - - public object GetService(Type serviceType) - { - return GetServices(serviceType).FirstOrDefault(); - } - - public IEnumerable GetServices(Type serviceType) - { - return _childContainer.ResolveAll(serviceType); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/IoC/GriffinWebApiDependencyResolver2.cs b/src/Server/OneTrueError.Web/App_Start/IoC/GriffinWebApiDependencyResolver2.cs deleted file mode 100644 index 2f522134..00000000 --- a/src/Server/OneTrueError.Web/App_Start/IoC/GriffinWebApiDependencyResolver2.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Http.Dependencies; -using Griffin.Container; - -namespace OneTrueError.Web.IoC -{ - public sealed class GriffinWebApiDependencyResolver2 : IDependencyResolver - { - private readonly Container _ioc; - - public GriffinWebApiDependencyResolver2(Container ioc) - { - _ioc = ioc; - } - - public void Dispose() - { - } - - public object GetService(Type serviceType) - { - return GetServices(serviceType).FirstOrDefault(); - } - - public IEnumerable GetServices(Type serviceType) - { - return _ioc.ResolveAll(serviceType); - } - - public IDependencyScope BeginScope() - { - return new GriffinWebApiChildScope(_ioc.CreateChildContainer()); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/LogConfigurator.cs b/src/Server/OneTrueError.Web/App_Start/LogConfigurator.cs deleted file mode 100644 index 73d638dc..00000000 --- a/src/Server/OneTrueError.Web/App_Start/LogConfigurator.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Configuration; -using System.IO; -using log4net.Config; - -namespace OneTrueError.Web -{ - public class LogConfigurator - { - public static void Configure() - { - var path = AppDomain.CurrentDomain.BaseDirectory; - var appType = ConfigurationManager.AppSettings["SiteType"]; - XmlConfigurator.Configure(new FileInfo(Path.Combine(path, "log4net." + appType + ".conf"))); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/RouteConfig.cs b/src/Server/OneTrueError.Web/App_Start/RouteConfig.cs deleted file mode 100644 index 9d6971ee..00000000 --- a/src/Server/OneTrueError.Web/App_Start/RouteConfig.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Web.Mvc; -using System.Web.Routing; - -namespace OneTrueError.Web -{ - public class RouteConfig - { - public static void RegisterInstallationRoutes(RouteCollection routes) - { - routes.MapRoute( - "Default", - "{controller}/{action}/{id}", - new {controller = "Home", action = "ToInstall", id = UrlParameter.Optional}, - new[] {"OneTrueError.Web.Controllers"} - ); - } - - public static void RegisterRoutes(RouteCollection routes) - { - routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); - //routes.RouteExistingFiles = true; - //routes.MapRoute("InstallationOff", - // "installation/{*catchAll}", - // new { controller = "Home", action = "NoInstallation" }); - routes.MapMvcAttributeRoutes(); - routes.MapRoute( - "Default", - "{controller}/{action}/{id}", - new {controller = "Home", action = "Index", id = UrlParameter.Optional}, - new[] {"OneTrueError.Web.Controllers"} - ); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/Services/ApplicationServiceManagerSettingsWithDefaultOn.cs b/src/Server/OneTrueError.Web/App_Start/Services/ApplicationServiceManagerSettingsWithDefaultOn.cs deleted file mode 100644 index b1691ae8..00000000 --- a/src/Server/OneTrueError.Web/App_Start/Services/ApplicationServiceManagerSettingsWithDefaultOn.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Configuration; -using Griffin.ApplicationServices; - -namespace OneTrueError.Web.Services -{ - /// - /// The default AppConfigServiceSettings will report off if the key is missing. We want the opposite. - /// - internal class ApplicationServiceManagerSettingsWithDefaultOn : ISettingsRepository - { - /// - public bool IsEnabled(Type type) - { - if (type == null) throw new ArgumentNullException("type"); - var value = ConfigurationManager.AppSettings[type.Name + ".Enabled"] ?? "true"; - return value == "true"; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/App_Start/Services/ServiceRunner.cs b/src/Server/OneTrueError.Web/App_Start/Services/ServiceRunner.cs deleted file mode 100644 index ee00bafb..00000000 --- a/src/Server/OneTrueError.Web/App_Start/Services/ServiceRunner.cs +++ /dev/null @@ -1,122 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using DotNetCqs; -using Griffin.ApplicationServices; -using Griffin.Container; -using Griffin.Data; -using log4net; -using OneTrueError.Web.Cqs; -using Owin; - -namespace OneTrueError.Web.Services -{ - /// - /// Used to configure the back-end. It's a mess, but a limited mess. - /// - public class ServiceRunner : IDisposable - { - private readonly CompositionRoot _compositionRoot = new CompositionRoot(); - private readonly CqsBuilder _cqsBuilder = new CqsBuilder(); - private readonly ILog _log = LogManager.GetLogger(typeof(ServiceRunner)); - private ApplicationServiceManager _appManager; - private BackgroundJobManager _backgroundJobManager; - - public IContainer Container - { - get { return CompositionRoot.Container; } - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public void Start(IAppBuilder app) - { - _log.Debug("Starting ..."); - try - { - _compositionRoot.Build(registrar => - { - registrar.RegisterService(x => _cqsBuilder.CreateCommandBus(Container), Lifetime.Singleton); - registrar.RegisterService(x => _cqsBuilder.CreateQueryBus(Container), Lifetime.Singleton); - registrar.RegisterService(x => _cqsBuilder.CreateEventBus(Container), Lifetime.Singleton); - - // let us guard it since it runs events in the background. - var service = registrar.Registrations.First(x => x.Implements(typeof(IEventBus))); - service.AddService(typeof(IApplicationService)); - - registrar.RegisterService(x => _cqsBuilder.CreateRequestReplyBus(Container), Lifetime.Singleton); - }); - - BuildServices(); - _appManager.Start(); - _backgroundJobManager.Start(); - _log.Debug("...started"); - } - catch (Exception exception) - { - _log.Error("Failed to start.", exception); - throw; - } - } - - public void Stop() - { - _backgroundJobManager.Stop(); - _appManager.Stop(); - } - - protected virtual void Dispose(bool isDisposing) - { - if (_backgroundJobManager != null) - { - _backgroundJobManager.Dispose(); - _backgroundJobManager = null; - } - } - - private void BuildServices() - { - _appManager = new ApplicationServiceManager(CompositionRoot.Container); - _appManager.Settings = new ApplicationServiceManagerSettingsWithDefaultOn(); - _appManager.ServiceFailed += OnServiceFailed; - - _backgroundJobManager = new BackgroundJobManager(CompositionRoot.Container); - _backgroundJobManager.JobFailed += OnJobFailed; - if (Debugger.IsAttached) - _backgroundJobManager.StartInterval = TimeSpan.FromSeconds(0); - else - _backgroundJobManager.StartInterval = TimeSpan.FromSeconds(10); - _backgroundJobManager.ExecuteInterval = TimeSpan.FromMinutes(5); - - _backgroundJobManager.ScopeClosing += OnScopeClosing; - } - - - private void OnJobFailed(object sender, BackgroundJobFailedEventArgs e) - { - _log.Error("Failed to execute " + e.Job, e.Exception); - } - - - private void OnScopeClosing(object sender, ScopeClosingEventArgs e) - { - try - { - e.Scope.Resolve().SaveChanges(); - } - catch (Exception exception) - { - _log.Error("Failed to close scope. Err: " + exception, e.Exception); - } - } - - private void OnServiceFailed(object sender, ApplicationServiceFailedEventArgs e) - { - _log.Error("Failed to execute " + e.ApplicationService, e.Exception); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/AdminAreaRegistration.cs b/src/Server/OneTrueError.Web/Areas/Admin/AdminAreaRegistration.cs deleted file mode 100644 index 71e94304..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/AdminAreaRegistration.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Web.Mvc; - -namespace OneTrueError.Web.Areas.Admin -{ - public class AdminAreaRegistration : AreaRegistration - { - public override string AreaName - { - get { return "Admin"; } - } - - public override void RegisterArea(AreaRegistrationContext context) - { - context.MapRoute( - "admin_default", - "admin/{controller}/{action}/{id}", - new {action = "Index", controller = "Home", id = UrlParameter.Optional}, - new[] {GetType().Namespace + ".Controllers"} - ); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/AdminMenu.cs b/src/Server/OneTrueError.Web/Areas/Admin/AdminMenu.cs deleted file mode 100644 index 4f12cf68..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/AdminMenu.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Web.Mvc; - -namespace OneTrueError.Web.Areas.Admin -{ - public static class AdminMenu - { - public static readonly MenuItem[] Steps = - { - new MenuItem("Start page", "~/admin"), - new MenuItem("Base configuration", "~/admin/home/basics/"), - new MenuItem("Error tracking", "~/admin/home/errors/"), - new MenuItem("Api keys", "~/admin/apikeys/"), - new MenuItem("Applications", "~/admin/application/"), - new MenuItem("Mail settings", "~/admin/messaging/email/"), - new MenuItem("Message queues", "~/admin/queues/"), - new MenuItem("Report settings", "~/admin/reporting/") - }; - - - public static string GetNextWizardStep(this UrlHelper urlHelper) - { - var index = FindCurrentIndex(urlHelper); - if (index == -1) - return null; - if (index < Steps.Length - 1) - index++; - - var step = Steps[index]; - return urlHelper.Content(step.VirtualPath); - } - - public static string GetNextWizardStepLink(this UrlHelper urlHelper) - { - var index = FindCurrentIndex(urlHelper); - if (index == -1) - return ""; - if (index < Steps.Length - 1) - index++; - - var step = Steps[index]; - return - $@"{step.Name} >>"; - } - - public static string GetPreviousWizardStepLink(this UrlHelper urlHelper) - { - var index = FindCurrentIndex(urlHelper); - if (index == -1) - return ""; - if (index > 0) - index--; - - var step = Steps[index]; - return - $@"<< {step.Name}"; - } - - public static bool IsCurrentStep(this UrlHelper urlHelper, MenuItem step) - { - var currentIndex = FindCurrentIndex(urlHelper); - var indexOfGivenStep = -1; - for (var i = 0; i < Steps.Length; i++) - { - if (Steps[i] == step) - { - indexOfGivenStep = i; - break; - } - } - - return currentIndex == indexOfGivenStep; - } - - private static int FindCurrentIndex(UrlHelper urlHelper) - { - var currentPath = urlHelper.RequestContext.HttpContext.Request.Url.AbsolutePath; - for (var i = 0; i < Steps.Length; i++) - { - if (Steps[i].IsForAbsolutePath(currentPath, urlHelper)) - return i; - } - - return -1; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ApiKeysController.cs b/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ApiKeysController.cs deleted file mode 100644 index 5a1c866c..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ApiKeysController.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using System.Web.Mvc; -using DotNetCqs; -using OneTrueError.Api.Core.ApiKeys.Commands; -using OneTrueError.Api.Core.ApiKeys.Queries; -using OneTrueError.Api.Core.Applications.Queries; -using OneTrueError.Infrastructure.Security; -using OneTrueError.Web.Areas.Admin.Models.ApiKeys; - -namespace OneTrueError.Web.Areas.Admin.Controllers -{ - public class ApiKeysController : Controller - { - private readonly ICommandBus _commandBus; - private readonly IQueryBus _queryBus; - - public ApiKeysController(IQueryBus queryBus, ICommandBus commandBus) - { - _queryBus = queryBus; - _commandBus = commandBus; - } - - public async Task Create() - { - var applications = await GetMyApplications(); - - var model = new CreateViewModel {Applications = applications}; - return View(model); - } - - [HttpPost] - public async Task Create(CreateViewModel model) - { - model.Applications = await GetMyApplications(); - if (!ModelState.IsValid) - return View(model); - - var apiKey = Guid.NewGuid().ToString("N"); - var sharedSecret = Guid.NewGuid().ToString("N"); - var apps = model.SelectedApplications == null - ? new int[0] - : model.SelectedApplications.Select(int.Parse).ToArray(); - var cmd = new CreateApiKey(model.ApplicationName, apiKey, sharedSecret, apps); - await _commandBus.ExecuteAsync(cmd); - - return RedirectToAction("Created", new {apiKey, sharedSecret}); - } - - public ActionResult Created(string apiKey, string sharedSecret) - { - ViewBag.ApiKey = apiKey; - ViewBag.SharedSecret = sharedSecret; - return View(); - } - - public async Task Delete(int id) - { - var cmd = new DeleteApiKey(id); - await _commandBus.ExecuteAsync(cmd); - return RedirectToAction("Deleted"); - } - - public ActionResult Deleted() - { - return View(); - } - - - public async Task Details(int id) - { - var key = await _queryBus.QueryAsync(new GetApiKey(id)); - return View(key); - } - - public async Task Index() - { - var query = new ListApiKeys(); - var result = await _queryBus.QueryAsync(query); - var vms = result.Keys.Select(x => new ListViewModelItem {Id = x.Id, Name = x.ApplicationName}).ToArray(); - var model = new ListViewModel {Keys = vms}; - return View(model); - } - - private async Task> GetMyApplications() - { - var query = new GetApplicationList - { - AccountId = User.GetAccountId(), - FilterAsAdmin = true - }; - var items = await _queryBus.QueryAsync(query); - var applications = items.ToDictionary(x => x.Id.ToString(), x => x.Name); - return applications; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ApplicationController.cs b/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ApplicationController.cs deleted file mode 100644 index 7f61c31f..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ApplicationController.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using System.Web.Mvc; -using System.Web.Routing; -using DotNetCqs; -using OneTrueError.Api.Core.Applications.Commands; -using OneTrueError.Api.Core.Applications.Queries; -using OneTrueError.Web.Areas.Admin.Models.Applications; - -namespace OneTrueError.Web.Areas.Admin.Controllers -{ - public class ApplicationController : Controller - { - private readonly ICommandBus _cmdBus; - private readonly IQueryBus _queryBus; - - - public ApplicationController(IQueryBus queryBus, ICommandBus cmdBus) - { - if (queryBus == null) throw new ArgumentNullException("queryBus"); - if (cmdBus == null) throw new ArgumentNullException("cmdBus"); - - _queryBus = queryBus; - _cmdBus = cmdBus; - } - - [HttpPost] - public async Task Delete(int id) - { - var cmd = new DeleteApplication(id); - await _cmdBus.ExecuteAsync(cmd); - await Task.Delay(500); - - var url = Url.Action("Deleted"); - return RedirectToAction("UpdateSession", "Account", new {area = "", returnUrl = url}); - } - - public ActionResult Deleted() - { - return View(); - } - - public async Task Edit(int id) - { - var query = new GetApplicationInfo(id); - var app = await _queryBus.QueryAsync(query); - var model = new EditViewModel - { - ApplicationId = app.Id, - Name = app.Name - }; - - return View(model); - } - - [HttpPost] - public async Task Edit(EditViewModel model) - { - if (!ModelState.IsValid) - return View(model); - - var cmd = new UpdateApplication(model.ApplicationId, model.Name); - await _cmdBus.ExecuteAsync(cmd); - - var dict = new RouteValueDictionary {{"usernote", "Application was updated"}}; - return RedirectToAction("Index", dict); - } - - public async Task Index() - { - var apps = await _queryBus.QueryAsync(new GetApplicationList()); - var model = apps.Select(x => new ApplicationViewModel {Id = x.Id, Name = x.Name}).ToList(); - return View(model); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/HomeController.cs b/src/Server/OneTrueError.Web/Areas/Admin/Controllers/HomeController.cs deleted file mode 100644 index f43194f9..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/HomeController.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Web.Mvc; -using OneTrueError.App.Configuration; -using OneTrueError.Client; -using OneTrueError.Infrastructure; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Web.Areas.Admin.Models; - -namespace OneTrueError.Web.Areas.Admin.Controllers -{ - public class HomeController : Controller - { - public ActionResult Basics() - { - var model = new BasicsViewModel(); - var config = ConfigurationStore.Instance.Load(); - if (config != null) - { - model.BaseUrl = config.BaseUrl.ToString(); - model.SupportEmail = config.SupportEmail; - } - else - { - model.BaseUrl = Request.Url.ToString().Replace("installation/setup/basics/", ""); - ViewBag.NextLink = ""; - } - - - return View(model); - } - - [HttpPost] - public ActionResult Basics(BasicsViewModel model) - { - var settings = new BaseConfiguration(); - settings.BaseUrl = new Uri(model.BaseUrl); - settings.SupportEmail = model.SupportEmail; - ConfigurationStore.Instance.Store(settings); - return Redirect(Url.GetNextWizardStep()); - } - - public ActionResult Errors() - { - var model = new ErrorTrackingViewModel(); - var config = ConfigurationStore.Instance.Load(); - if (config != null) - { - model.ActivateTracking = config.ActivateTracking; - model.ContactEmail = config.ContactEmail; - model.InstallationId = config.InstallationId; - } - else - ViewBag.NextLink = ""; - - return View("ErrorTracking", model); - } - - [HttpPost] - public ActionResult Errors(ErrorTrackingViewModel model) - { - if (!ModelState.IsValid) - return View("ErrorTracking", model); - - var settings = new OneTrueErrorConfigSection(); - settings.ActivateTracking = model.ActivateTracking; - settings.ContactEmail = model.ContactEmail; - settings.InstallationId = model.InstallationId; - ConfigurationStore.Instance.Store(settings); - WebApiApplication.ReportToOneTrueError = model.ActivateTracking; - return Redirect(Url.GetNextWizardStep()); - } - - // GET: Installation/Home - public ActionResult Index() - { - try - { - ConnectionFactory.Create(); - } - catch - { - ViewBag.Ready = false; - } - return View(); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/MessagingController.cs b/src/Server/OneTrueError.Web/Areas/Admin/Controllers/MessagingController.cs deleted file mode 100644 index 5be2eaea..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/MessagingController.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Web.Mvc; -using OneTrueError.App.Modules.Messaging.Commands; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Web.Areas.Admin.Models; - -namespace OneTrueError.Web.Areas.Admin.Controllers -{ - public class MessagingController : Controller - { - public ActionResult Email() - { - var model = new EmailViewModel(); - var settings = ConfigurationStore.Instance.Load(); - if (settings == null || string.IsNullOrEmpty(settings.SmtpHost)) - return View(model); - - model.AccountName = settings.AccountName; - model.PortNumber = settings.PortNumber; - model.SmtpHost = settings.SmtpHost; - model.UseSSL = settings.UseSsl; - model.AccountPassword = settings.AccountPassword; - return View(model); - } - - [HttpPost] - public ActionResult Email(EmailViewModel model) - { - if (!ModelState.IsValid) - return View(model); - - var settings = new DotNetSmtpSettings - { - AccountName = model.AccountName, - PortNumber = model.PortNumber ?? 25, - AccountPassword = model.AccountPassword, - SmtpHost = model.SmtpHost, - UseSsl = model.UseSSL - }; - ConfigurationStore.Instance.Store(settings); - return Redirect(Url.GetNextWizardStep()); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/QueuesController.cs b/src/Server/OneTrueError.Web/Areas/Admin/Controllers/QueuesController.cs deleted file mode 100644 index 7e192db1..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/QueuesController.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Web.Mvc; -using OneTrueError.Infrastructure; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.Web.Areas.Admin.Models; - -namespace OneTrueError.Web.Areas.Admin.Controllers -{ - public class QueuesController : Controller - { - public ActionResult Index() - { - var model = new QueueViewModel(); - - var settings = ConfigurationStore.Instance.Load(); - if (settings == null) - { - model.UseSql = true; - return View(model); - } - - model.UseSql = settings.UseSql; - model.EventQueue = settings.EventQueue; - model.EventAuthentication = settings.EventAuthentication; - model.EventTransactions = settings.EventTransactions; - model.EventQueue = settings.ReportQueue; - model.ReportTransactions = settings.ReportTransactions; - model.ReportAuthentication = settings.ReportAuthentication; - model.FeedbackQueue = settings.FeedbackQueue; - model.FeedbackAuthentication = settings.FeedbackAuthentication; - model.FeedbackTransactions = settings.FeedbackTransactions; - - return View(model); - } - - - [HttpPost] - public ActionResult Index(QueueViewModel model) - { - var config = new MessageQueueSettings(); - if (model.UseSql) - { - config.UseSql = true; - ConfigurationStore.Instance.Store(config); - return Redirect(Url.GetNextWizardStep()); - } - - var errorMessage = ""; - if ( - !SetupTools.ValidateMessageQueue(model.ReportQueue, model.ReportAuthentication, model.ReportTransactions, - out errorMessage)) - { - ModelState.AddModelError("ReportQueue", errorMessage); - } - if ( - !SetupTools.ValidateMessageQueue(model.FeedbackQueue, model.FeedbackAuthentication, - model.FeedbackTransactions, out errorMessage)) - { - ModelState.AddModelError("FeedbackQueue", errorMessage); - } - if ( - !SetupTools.ValidateMessageQueue(model.EventQueue, model.EventAuthentication, model.EventTransactions, - out errorMessage)) - { - ModelState.AddModelError("EventQueue", errorMessage); - } - - if (!ModelState.IsValid) - { - return View(model); - } - - config.EventQueue = model.EventQueue; - config.EventAuthentication = model.EventAuthentication; - config.EventTransactions = model.EventTransactions; - config.EventQueue = model.ReportQueue; - config.ReportTransactions = model.ReportTransactions; - config.ReportAuthentication = model.ReportAuthentication; - config.FeedbackQueue = model.FeedbackQueue; - config.FeedbackAuthentication = model.FeedbackAuthentication; - config.FeedbackTransactions = model.FeedbackTransactions; - ConfigurationStore.Instance.Store(config); - - return Redirect(Url.GetNextWizardStep()); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ReportingController.cs b/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ReportingController.cs deleted file mode 100644 index 96fdb85c..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Controllers/ReportingController.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; -using System.Web.Mvc; -using log4net; -using OneTrueError.App.Core.Reports.Config; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Web.Areas.Admin.Models; - -namespace OneTrueError.Web.Areas.Admin.Controllers -{ - public class ReportingController : Controller - { - private ILog _logger = LogManager.GetLogger(typeof(ReportingController)); - - [HttpGet] - public ActionResult Index() - { - var model = new ReportingViewModel(); - var settings = ConfigurationStore.Instance.Load(); - if (settings == null || settings.MaxReportsPerIncident == 0) - return View(model); - - _logger.Debug("Display acess: " + settings.MaxReportsPerIncident + " from " + ConfigurationStore.Instance.GetHashCode()); - model.MaxReportsPerIncident = settings.MaxReportsPerIncident; - model.RetentionDays= settings.RetentionDays; - return View(model); - } - - [HttpPost] - public ActionResult Index(ReportingViewModel model) - { - if (!ModelState.IsValid) - return View(model); - - var settings = new ReportConfig - { - MaxReportsPerIncident = model.MaxReportsPerIncident, - RetentionDays = model.RetentionDays, - }; - _logger.Debug("Storing: " + settings.MaxReportsPerIncident); - ConfigurationStore.Instance.Store(settings); - _logger.Debug("Stored: " + settings.MaxReportsPerIncident + " to " + ConfigurationStore.Instance.GetHashCode()); - - return RedirectToAction("Index", "Home"); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/MenuItem.cs b/src/Server/OneTrueError.Web/Areas/Admin/MenuItem.cs deleted file mode 100644 index a9d04ae2..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/MenuItem.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Web.Mvc; - -namespace OneTrueError.Web.Areas.Admin -{ - public class MenuItem - { - public MenuItem(string name, string virtualPath) - { - Name = name; - VirtualPath = virtualPath; - } - - public string Name { get; set; } - - public string VirtualPath { get; set; } - - public bool IsForAbsolutePath(string currentPath, UrlHelper helper) - { - var myPath = helper.Content(VirtualPath).TrimEnd('/'); - currentPath = currentPath.TrimEnd('/'); - return myPath.Equals(currentPath, StringComparison.OrdinalIgnoreCase); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/AccountViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/AccountViewModel.cs deleted file mode 100644 index 69627401..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/AccountViewModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Admin.Models -{ - public class AccountViewModel - { - [Required, StringLength(255)] - public string EmailAddress { get; set; } - - [Required, StringLength(40)] - public string Password { get; set; } - - [Required, StringLength(40)] - public string UserName { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/CreateViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/CreateViewModel.cs deleted file mode 100644 index 00f508b7..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/CreateViewModel.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Admin.Models.ApiKeys -{ - public class CreateViewModel - { - [Required, Display(Name = "Application name")] - public string ApplicationName { get; set; } - - public Dictionary Applications { get; set; } - - public bool ReadOnly { get; set; } - - [Display(Name = "Selected applications")] - public string[] SelectedApplications { get; set; } - } -} - -/*function S4() { - return (((1+Math.random())*0x10000)|0).toString(16).substring(1); -} - -// then to call it, plus stitch in '4' in the third group -guid = (S4() + S4() + "-" + S4() + "-4" + S4().substr(0,3) + "-" + S4() + "-" + S4() + S4() + S4()).toLowerCase(); -*/ \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/ListViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/ListViewModel.cs deleted file mode 100644 index 89bc656d..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/ListViewModel.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace OneTrueError.Web.Areas.Admin.Models.ApiKeys -{ - public class ListViewModel - { - public ListViewModelItem[] Keys { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/ListViewModelItem.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/ListViewModelItem.cs deleted file mode 100644 index 79c69a10..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/ApiKeys/ListViewModelItem.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OneTrueError.Web.Areas.Admin.Models.ApiKeys -{ - public class ListViewModelItem - { - public int Id { get; set; } - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/Applications/ApplicationViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/Applications/ApplicationViewModel.cs deleted file mode 100644 index 400718fa..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/Applications/ApplicationViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web; - -namespace OneTrueError.Web.Areas.Admin.Models.Applications -{ - public class ApplicationViewModel - { - public int Id { get; set; } - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/Applications/EditViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/Applications/EditViewModel.cs deleted file mode 100644 index 2eccc6ef..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/Applications/EditViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace OneTrueError.Web.Areas.Admin.Models.Applications -{ - public class EditViewModel - { - public int ApplicationId { get; set; } - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/BasicsViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/BasicsViewModel.cs deleted file mode 100644 index 97b40a36..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/BasicsViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Admin.Models -{ - public class BasicsViewModel - { - [Required, MinLength(4)] - public string BaseUrl { get; set; } - - [Required, EmailAddress] - public string SupportEmail { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/EmailViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/EmailViewModel.cs deleted file mode 100644 index db468a15..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/EmailViewModel.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Admin.Models -{ - public class EmailViewModel - { - [Display(Name = "Account Name")] - public string AccountName { get; set; } - - [Display(Name = "Account password")] - public string AccountPassword { get; set; } - - [Display(Name = "SMTP Port"), Required] - public int? PortNumber { get; set; } - - [Display(Name = "SMTP Host"), Required] - public string SmtpHost { get; set; } - - [Display(Name = "Use SSL")] - public bool UseSSL { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/ErrorTrackingViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/ErrorTrackingViewModel.cs deleted file mode 100644 index 924f4521..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/ErrorTrackingViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Admin.Models -{ - public class ErrorTrackingViewModel - { - [Display(Name = "Activate tracking")] - public bool ActivateTracking { get; set; } - - [Display(Name = "Contact email"), EmailAddress] - public string ContactEmail { get; set; } - - /// - /// A fixed identity which identifies this specific installation. You can generate a GUID and then store it. - /// - /// - /// - /// Used to identify the number of installations that have the same issue. - /// - /// - public string InstallationId { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/QueueViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/QueueViewModel.cs deleted file mode 100644 index 04ea400e..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/QueueViewModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace OneTrueError.Web.Areas.Admin.Models -{ - public class QueueViewModel - { - public bool EventAuthentication { get; set; } - - public string EventQueue { get; set; } - - public bool EventTransactions { get; set; } - - public bool FeedbackAuthentication { get; set; } - - public string FeedbackQueue { get; set; } - - public bool FeedbackTransactions { get; set; } - - public bool ReportAuthentication { get; set; } - public string ReportQueue { get; set; } - - public bool ReportTransactions { get; set; } - - public bool UseSql { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Models/ReportingViewModel.cs b/src/Server/OneTrueError.Web/Areas/Admin/Models/ReportingViewModel.cs deleted file mode 100644 index 8bc9199b..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Models/ReportingViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Web; - -namespace OneTrueError.Web.Areas.Admin.Models -{ - public class ReportingViewModel - { - public ReportingViewModel() - { - MaxReportsPerIncident = 500; - RetentionDays = 90; - } - - [Required, Range(1, 10000)] - public int MaxReportsPerIncident { get; set; } - - [Required, Range(1, 365)] - public int RetentionDays { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Create.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Create.cshtml deleted file mode 100644 index 8a7acf6c..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Create.cshtml +++ /dev/null @@ -1,39 +0,0 @@ -@model OneTrueError.Web.Areas.Admin.Models.ApiKeys.CreateViewModel -@{ - ViewBag.Title = "Create"; -} -
-
-

Create a new API key

-

- -

-
- @Html.ValidationSummary(false) -
- @Html.LabelFor(x => x.ApplicationName, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.ApplicationName, new {@class = "form-control", placeholder = "Application name"}) - -
- @*
- -
*@ -
- @Html.LabelFor(x => x.SelectedApplications, new {@class = "control-label"}) -
- Select the applications that this ApiKey can access. (none selected = can access all) -
- @foreach (var app in Model.Applications) - { - - @app.Value
- } -
-
- -
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Created.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Created.cshtml deleted file mode 100644 index da588a5e..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Created.cshtml +++ /dev/null @@ -1,28 +0,0 @@ -@{ - ViewBag.Title = "ApiKey was created"; -} - - -
-
-
-

Api key

-
The API key have been created.
-

- To invoke the server API pass the API key in a HttpHeader called X-Api-Key. Use the shared secret to calculate a HMAC signature on the HTTP Request body and include it in the http header X-Api-Signature. -

-
-
Api key
-
@ViewBag.ApiKey
-
Shared secret
-
@ViewBag.SharedSecret
-
- -

Example

-
var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_sharedSecret));
-var hash = hmac.ComputeHash(httpBodyAsByteArray);
-var signature = Convert.ToBase64String(hash);
-
-
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Deleted.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Deleted.cshtml deleted file mode 100644 index 15bd2406..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Deleted.cshtml +++ /dev/null @@ -1,13 +0,0 @@ -@{ - ViewBag.Title = "ApiKey is deleted"; -} - - -
-
-
-

Api key

-
The ApiKey has been deleted.
-
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Details.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Details.cshtml deleted file mode 100644 index 7ae2ea77..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Details.cshtml +++ /dev/null @@ -1,68 +0,0 @@ -@model OneTrueError.Api.Core.ApiKeys.Queries.GetApiKeyResult -@{ - ViewBag.Title = "Api key"; -} - - -
-
-
-

Api key

-
-

- To invoke the server API pass the API key in a HttpHeader called X-Api-Key. Use the shared secret to calculate a HMAC signature on the HTTP Request body and include it in the http header X-Api-Signature. -

-
-
-
Api key
-
@Model.GeneratedKey
-
Shared secret
-
@Model.SharedSecret
-
Created by
-
- -
-
-
-
-

Authorized applications

-

The key is authorized to modify the following applications:

-
    - @foreach (var app in Model.AllowedApplications) - { -
  • @app.ApplicationName
  • - } -
-
- -

Signing example

-
var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_sharedSecret));
-var hash = hmac.ComputeHash(httpBodyAsByteArray);
-var signature = Convert.ToBase64String(hash);
-
-
-
-
- - -
-
-
-
-
-@section scripts -{ - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Index.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Index.cshtml deleted file mode 100644 index 0098fa94..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/ApiKeys/Index.cshtml +++ /dev/null @@ -1,22 +0,0 @@ -@model OneTrueError.Web.Areas.Admin.Models.ApiKeys.ListViewModel -@{ - ViewBag.Title = "Admin - Api keys"; -} -
-
-

Api keys

-

- Api keys are used when communicating with OneTrueError through the API. -

- Create a new key -

Existing keys

-
    - @foreach (var item in Model.Keys) - { -
  • - @item.Name -
  • - } -
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Deleted.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Deleted.cshtml deleted file mode 100644 index ed4d5dc7..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Deleted.cshtml +++ /dev/null @@ -1,13 +0,0 @@ -@{ - ViewBag.Title = "Application is deleted"; -} - - -
-
-
-

Application

-
The application has been deleted.
-
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Edit.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Edit.cshtml deleted file mode 100644 index 98a9cf8b..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Edit.cshtml +++ /dev/null @@ -1,22 +0,0 @@ -@model OneTrueError.Web.Areas.Admin.Models.Applications.EditViewModel -@{ - ViewBag.Title = "Edit application"; -} - - -
-
-
-
-

Edit application

- @Html.HiddenFor(x => x.ApplicationId) -
- -
-
- -
-
-
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Index.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Index.cshtml deleted file mode 100644 index 81e86ce0..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Application/Index.cshtml +++ /dev/null @@ -1,45 +0,0 @@ -@model IList -@{ - ViewBag.Title = "Admin - Applications"; -} -
-
-

Applications

-

- Here you can delete applications, which also will delete all data which have been associated with them. -

- - - - - - @foreach (var item in Model) - { - - - - - } - -
NameOptions
@item.Name - Rename - Delete -
-
-
-
- -
-@section scripts{ - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/Basics.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/Basics.cshtml deleted file mode 100644 index fae6e582..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/Basics.cshtml +++ /dev/null @@ -1,30 +0,0 @@ -@model OneTrueError.Web.Areas.Admin.Models.BasicsViewModel -@{ - ViewBag.Title = "Admin - Basics"; -} -
-
- -

Base configuration

-
- @Html.ValidationSummary(false) -
- - - Address used when visiting this site. -
-
- - - Used by your users when they need support using OneTrueError, for instance for account troubles. Also used as sender in outbound emails. -
-
- -
-
-
diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/ErrorTracking.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/ErrorTracking.cshtml deleted file mode 100644 index a2e8c96d..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/ErrorTracking.cshtml +++ /dev/null @@ -1,60 +0,0 @@ -@model OneTrueError.Web.Areas.Admin.Models.ErrorTrackingViewModel -@{ - ViewBag.Title = "Admin - Error tracking"; -} -
-
- -

Error tracking

-

- To correct bugs much faster we would like to activate OneTrueError for your installation. All exceptions - will be uploaded to our own installation for further analysis. -

-
- @Html.ValidationSummary(false) -
- @Html.CheckBoxFor(x => x.ActivateTracking, new {@class = "form-control", style = "display:inline;height:auto;width:inherit;"}) - @Html.LabelFor(x => x.ActivateTracking, new {@class = "control-label"}) -
- Allow us to track errors. -
-
- @Html.LabelFor(x => x.ContactEmail, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.ContactEmail, new {@class = "form-control", disabled = ""}) - Email address that we may contact if we need any further information (will also receive notifications when your errors have been corrected). -
-
- @Html.LabelFor(x => x.InstallationId, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.InstallationId, new {@class = "form-control", disabled = ""}) - A fixed identity which identifies this specific installation. You can generate a GUID and then store it. Used to identify the number of installations that have the same issue. -
- A guid generated for your convencience if you want to enable this feature: @Guid.NewGuid().ToString("N") -
-
- -
-
-
-@section scripts -{ - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/Index.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/Index.cshtml deleted file mode 100644 index f11464b5..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Home/Index.cshtml +++ /dev/null @@ -1,15 +0,0 @@ -@{ - ViewBag.Title = "Adminarea - start page"; -} -
-
- -

Administration area

- -

Welcome to the OneTrueError admin area.

-

- Most of the settings are stored in the Settings table in the database. -

-
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Messaging/Email.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Messaging/Email.cshtml deleted file mode 100644 index f86f020a..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Messaging/Email.cshtml +++ /dev/null @@ -1,54 +0,0 @@ -@model OneTrueError.Web.Areas.Admin.Models.EmailViewModel -@{ - ViewBag.Title = "Admin - Email configuration"; -} -
-
- -

Email configuration

-

- OneTrueError can send email notifications upon different types of events (and password resets etc). To do this, we need - to have a SMTP account for mailing. -

-
- @Html.ValidationSummary(false) -
- @Html.LabelFor(x => x.SmtpHost, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.SmtpHost, new {@class = "form-control", placeholder = "IP Address or host name"}) - @Html.CheckBoxFor(x => x.UseSSL) Use SSL -
-
- @Html.LabelFor(x => x.PortNumber, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.PortNumber, new {@class = "form-control", placeholder = "Port number. Typically 25 for SMTP and 587 for ESMTPS. Consult your mail server configuration."}) -
-
- @Html.LabelFor(x => x.AccountName, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.AccountName, new {@class = "form-control", placeholder = "SMTP Server account name (empty = no authentication)"}) -
-
- @Html.LabelFor(x => x.AccountPassword, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.AccountPassword, new {@class = "form-control", olaceholder = "Password for the above account"}) -
-
- - -
-
-
- -
-
-
-@section scripts -{ - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Queues/Index.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Queues/Index.cshtml deleted file mode 100644 index 9a47299d..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Queues/Index.cshtml +++ /dev/null @@ -1,97 +0,0 @@ -@model OneTrueError.Web.Areas.Admin.Models.QueueViewModel -@{ - ViewBag.Title = "Installation - Queues"; -} -
-
- -

Message queues

-

- To get a bit of separation in the project we are using messaging between different - concerns in the code. The queues also give you an indication on how busy the system is - analyzing reports. -

-

We are currently using three different queues.

-
    -
  • A queue to store inbound reports before they are being processed
  • -
  • A queue to store inbound error descriptions (user feedback)
  • -
  • A queue for all internally published events.
  • -
-

- You currently have two alternatives for this. The first alternative is to use - MSMQ. In this case you need to configure three MSMQ (a guide) queues for this. - The other alternative is to use tables in the SQL database, those can be created by this setup. -

-

- If you want to make sure that all reports are received ASAP at all times we recommend MSMQ queues as - they are run om the local system they will be processed as long as the server is up (no dependency on the SQL server). By using - MSMQ you can also configure a monitoring software to have alarms on the queues to see if something goes wrong. -

-
- @Html.ValidationSummary(false) - -
- -
- -
-
-
- -@section scripts -{ - -} diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Reporting/Index.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Reporting/Index.cshtml deleted file mode 100644 index 078e6001..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Reporting/Index.cshtml +++ /dev/null @@ -1,30 +0,0 @@ -@model OneTrueError.Web.Areas.Admin.Models.ReportingViewModel -@{ - ViewBag.Title = "Admin - Reporting"; -} -
-
- -

Base configuration

-
- @Html.ValidationSummary(false) -
- - - Start deleting the oldest reports for an incident when this number of reports have been received for it. -
-
- - - Keep a report this long for an incident. -
-
- -
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Shared/_Layout.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Shared/_Layout.cshtml deleted file mode 100644 index 8bab0738..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Shared/_Layout.cshtml +++ /dev/null @@ -1,75 +0,0 @@ -@using OneTrueError.Web.Areas.Admin - - - - - - @ViewBag.Title - OneTrueError - - @Styles.Render("~/Content/css") - @Scripts.Render("~/bundles/modernizr") - @RenderSection("Styles", false) - - - - - -
-
- -
-

Settings

-
    - @foreach (var step in AdminMenu.Steps) - { -
  • - @step.Name -
  • - } - -
-
-
-
- @RenderBody() -
-
-
-
-
-
- -
-
-
-@Scripts.Render("~/bundles/jquery") -@Scripts.Render("~/bundles/bootstrap") -@RenderSection("scripts", false) - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Sql/Index.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Sql/Index.cshtml deleted file mode 100644 index 990e8d88..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Sql/Index.cshtml +++ /dev/null @@ -1,65 +0,0 @@ -@{ - ViewBag.Title = "Installation - Database configuration"; -} -
-
-
-

Database configuration

- -

- It's time to to configure the database. To do that you need - to start by specifying which database to use. We expect that you have created - a database and configured an account for it. -

-

- Modify the connectionString named 'Db' in web.config. Click on 'Test Connection' to make sure that it works. -

-
-
-

- @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
-
-
- -
-
-

Limitation

-

- - Currently only Microsoft SQL Server 2012 and above is supported. Need any other DB? Feel free to Contribute - by taking the SqlServer class library and convert it to a library for your favorite DB engine. - -

-

Example

-
Data Source=(localdb)\ProjectsV12;Initial Catalog=OneTrueError;Integrated Security=True;Connect Timeout=30;
-
-
-
- -@section scripts -{ - - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/Sql/Tables.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/Sql/Tables.cshtml deleted file mode 100644 index dd2b81be..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/Sql/Tables.cshtml +++ /dev/null @@ -1,38 +0,0 @@ -@{ - ViewBag.Title = "Tables"; -} - -
-
-

Tables

- -

It's time to install all the tables. Keep your fingers crossed and press "Create Tables"

- - @if (ViewBag.GotTables) - { -
- Tables have already been added/updated. -
- @Html.Raw(ViewBag.PrevLink) - @Html.Raw(ViewBag.NextLink) - } - else - { -
- @Html.ValidationSummary(true) - - @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
- } - @if (ViewBag.GotException) - { -

Error details

-
-
@ViewBag.FullException
-
-
- } -
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/_ViewStart.cshtml b/src/Server/OneTrueError.Web/Areas/Admin/Views/_ViewStart.cshtml deleted file mode 100644 index c3d24e51..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/_ViewStart.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@{ - Layout = "~/Areas/Admin/Views/Shared/_Layout.cshtml"; -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Admin/Views/web.config b/src/Server/OneTrueError.Web/Areas/Admin/Views/web.config deleted file mode 100644 index 967492da..00000000 --- a/src/Server/OneTrueError.Web/Areas/Admin/Views/web.config +++ /dev/null @@ -1,43 +0,0 @@ - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Controllers/AccountController.cs b/src/Server/OneTrueError.Web/Areas/Installation/Controllers/AccountController.cs deleted file mode 100644 index 74a1bc1b..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Controllers/AccountController.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using System.Web; -using System.Web.Mvc; -using Griffin.Data; -using Microsoft.Owin.Security; -using OneTrueError.Api.Core.Applications; -using OneTrueError.App.Core.Accounts; -using OneTrueError.App.Core.Applications; -using OneTrueError.App.Core.Users; -using OneTrueError.Infrastructure; -using OneTrueError.Infrastructure.Security; -using OneTrueError.SqlServer.Core.Accounts; -using OneTrueError.SqlServer.Core.Applications; -using OneTrueError.SqlServer.Core.Users; -using OneTrueError.Web.Areas.Installation.Models; - -namespace OneTrueError.Web.Areas.Installation.Controllers -{ - public class AccountController : Controller - { - public ActionResult Admin() - { - SetStateFlag(); - var model = new AccountViewModel(); - return View(model); - } - - - [HttpPost] - public async Task Admin(AccountViewModel model) - { - SetStateFlag(); - - if (!ModelState.IsValid) - return View(model); - - try - { - var account = new Account(model.UserName, model.Password); - account.Activate(); - var con = SetupTools.DbTools.OpenConnection(); - var uow = new AdoNetUnitOfWork(con); - var repos = new AccountRepository(uow); - if (await repos.IsUserNameTakenAsync(model.UserName)) - return Redirect(Url.GetNextWizardStep()); - - account.SetVerifiedEmail(model.EmailAddress); - await repos.CreateAsync(account); - - var user = new User(account.Id, account.UserName) - { - EmailAddress = account.Email - }; - var userRepos = new UserRepository(uow); - await userRepos.CreateAsync(user); - - var repos2 = new ApplicationRepository(uow); - var app = new Application(user.AccountId, "DemoApp") - { - ApplicationType = TypeOfApplication.DesktopApplication - }; - await repos2.CreateAsync(app); - - var tm = new ApplicationTeamMember(app.Id, account.Id) - { - AddedByName = "System", - Roles = new[] {ApplicationRole.Admin, ApplicationRole.Member}, - UserName = account.UserName - }; - await repos2.CreateAsync(tm); - - uow.SaveChanges(); - - var claims = new List - { - new Claim(ClaimTypes.NameIdentifier, account.Id.ToString(), ClaimValueTypes.Integer32), - new Claim(ClaimTypes.Name, account.UserName, ClaimValueTypes.String), - new Claim(ClaimTypes.Email, account.Email, ClaimValueTypes.String), - new Claim(OneTrueClaims.Application, app.Id.ToString(), ClaimValueTypes.Integer32), - new Claim(OneTrueClaims.ApplicationAdmin, app.Id.ToString(), ClaimValueTypes.Integer32), - new Claim(ClaimTypes.Role, OneTrueClaims.RoleSysAdmin, ClaimValueTypes.String) - }; - var identity = new ClaimsIdentity(claims, "Cookie", ClaimTypes.Name, ClaimTypes.Role); - var properties = new AuthenticationProperties {IsPersistent = false}; - HttpContext.GetOwinContext().Authentication.SignIn(properties, identity); - - return Redirect(Url.GetNextWizardStep()); - } - catch (Exception ex) - { - ViewBag.Exception = ex; - ModelState.AddModelError("", ex.Message); - return View(model); - } - } - - protected override void OnActionExecuting(ActionExecutingContext filterContext) - { - ViewBag.PrevLink = Url.GetPreviousWizardStepLink(); - ViewBag.NextLink = Url.GetNextWizardStepLink(); - base.OnActionExecuting(filterContext); - } - - private void SetStateFlag() - { - ViewBag.Exception = null; - ViewBag.AlreadyCreated = false; - if (User.Identity.IsAuthenticated) - ViewBag.AlreadyCreated = true; - else - { - using (var con = SetupTools.DbTools.OpenConnection()) - { - using (var uow = new AdoNetUnitOfWork(con)) - { - var id = uow.ExecuteScalar("SELECT TOP 1 Id FROM Accounts"); - if (id != null) - ViewBag.AlreadyCreated = true; - } - } - } - - if (!ViewBag.AlreadyCreated) - ViewBag.NextLink = null; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Controllers/MessagingController.cs b/src/Server/OneTrueError.Web/Areas/Installation/Controllers/MessagingController.cs deleted file mode 100644 index 8aae5ac5..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Controllers/MessagingController.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Web.Mvc; -using OneTrueError.App.Modules.Messaging.Commands; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Web.Areas.Installation.Models; - -namespace OneTrueError.Web.Areas.Installation.Controllers -{ - public class MessagingController : Controller - { - public ActionResult Email() - { - var model = new EmailViewModel(); - var settings = ConfigurationStore.Instance.Load(); - if (!string.IsNullOrEmpty(settings?.SmtpHost)) - { - model.AccountName = settings.AccountName; - model.PortNumber = settings.PortNumber; - model.SmtpHost = settings.SmtpHost; - model.UseSSL = settings.UseSsl; - model.AccountPassword = settings.AccountPassword; - } - else - { - ViewBag.NextLink = ""; - } - - return View(model); - } - - [HttpPost] - public ActionResult Email(EmailViewModel model) - { - if (!ModelState.IsValid) - return View(model); - - var settings = new DotNetSmtpSettings - { - AccountName = model.AccountName, - PortNumber = model.PortNumber ?? 25, - AccountPassword = model.AccountPassword, - SmtpHost = model.SmtpHost, - UseSsl = model.UseSSL - }; - ConfigurationStore.Instance.Store(settings); - return Redirect(Url.GetNextWizardStep()); - } - - protected override void OnActionExecuting(ActionExecutingContext filterContext) - { - ViewBag.PrevLink = Url.GetPreviousWizardStepLink(); - ViewBag.NextLink = Url.GetNextWizardStepLink(); - base.OnActionExecuting(filterContext); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Controllers/SetupController.cs b/src/Server/OneTrueError.Web/Areas/Installation/Controllers/SetupController.cs deleted file mode 100644 index 9d864298..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Controllers/SetupController.cs +++ /dev/null @@ -1,155 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Net.Http; -using System.Threading.Tasks; -using System.Web.Mvc; -using OneTrueError.App.Configuration; -using OneTrueError.Infrastructure; -using OneTrueError.Infrastructure.Configuration; -using OneTrueError.Web.Areas.Installation.Models; - -namespace OneTrueError.Web.Areas.Installation.Controllers -{ - public class SetupController : Controller - { - [HttpPost, AllowAnonymous] - public ActionResult Activate() - { - ConfigurationManager.RefreshSection("appSettings"); - if (ConfigurationManager.AppSettings["Configured"] != "true") - { - return RedirectToAction("Completed", new - { - displayError = 1 - }); - } - return Redirect("~/?#/welcome"); - } - - public ActionResult Support() - { - return View(new SupportViewModel()); - } - - [HttpPost] - public async Task Support(SupportViewModel model) - { - if (!ModelState.IsValid) - return View(model); - - try - { - var client = new HttpClient(); - var content = - new FormUrlEncodedContent(new [] - { - new KeyValuePair("EmailAddress", model.Email), - new KeyValuePair("CompanyName", model.CompanyName) - }); - await client.PostAsync("https://onetrueerror.com/support/register/", content); - return Redirect(Url.GetNextWizardStep()); - } - catch (Exception ex) - { - ModelState.AddModelError("", ex.Message); - return View(model); - } - } - - public ActionResult Basics() - { - var model = new BasicsViewModel(); - var config = ConfigurationStore.Instance.Load(); - if (config != null) - { - model.BaseUrl = config.BaseUrl.ToString(); - model.SupportEmail = config.SupportEmail; - } - else - { - model.BaseUrl = Request.Url.ToString().Replace("installation/setup/basics/", "").Replace("localhost", "yourServerName"); - ViewBag.NextLink = ""; - } - - - return View(model); - } - - [HttpPost] - public ActionResult Basics(BasicsViewModel model) - { - var settings = new BaseConfiguration(); - if (!model.BaseUrl.EndsWith("/")) - model.BaseUrl += "/"; - - if (model.BaseUrl.IndexOf("localhost", StringComparison.OrdinalIgnoreCase) != -1) - { - ModelState.AddModelError("BaseUrl", "Use the servers real DNS name instead of 'localhost'. If you don't the Ajax request wont work as CORS would be enforced by IIS."); - return View(model); - } - settings.BaseUrl = new Uri(model.BaseUrl); - settings.SupportEmail = model.SupportEmail; - ConfigurationStore.Instance.Store(settings); - return Redirect(Url.GetNextWizardStep()); - } - - - public ActionResult Completed(string displayError = null) - { - ViewBag.DisplayError = displayError == "1"; - return View(); - } - - public ActionResult Errors() - { - var model = new ErrorTrackingViewModel(); - var config = ConfigurationStore.Instance.Load(); - if (config != null) - { - model.ActivateTracking = config.ActivateTracking; - model.ContactEmail = config.ContactEmail; - model.InstallationId = config.InstallationId; - } - else - ViewBag.NextLink = ""; - - return View("ErrorTracking", model); - } - - [HttpPost] - public ActionResult Errors(ErrorTrackingViewModel model) - { - if (!ModelState.IsValid) - return View("ErrorTracking", model); - - var settings = new OneTrueErrorConfigSection(); - settings.ActivateTracking = model.ActivateTracking; - settings.ContactEmail = model.ContactEmail; - settings.InstallationId = model.InstallationId; - ConfigurationStore.Instance.Store(settings); - return Redirect(Url.GetNextWizardStep()); - } - - // GET: Installation/Home - public ActionResult Index() - { - try - { - ConnectionFactory.Create(); - } - catch - { - ViewBag.Ready = false; - } - return View(); - } - - protected override void OnActionExecuting(ActionExecutingContext filterContext) - { - ViewBag.PrevLink = Url.GetPreviousWizardStepLink(); - ViewBag.NextLink = Url.GetNextWizardStepLink(); - base.OnActionExecuting(filterContext); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Controllers/SqlController.cs b/src/Server/OneTrueError.Web/Areas/Installation/Controllers/SqlController.cs deleted file mode 100644 index 0c39f5cd..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Controllers/SqlController.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System; -using System.Configuration; -using System.Web.Mvc; -using Griffin.Data; -using OneTrueError.App.Core.Applications; -using OneTrueError.Infrastructure; -using OneTrueError.SqlServer.Core.Applications; - -namespace OneTrueError.Web.Areas.Installation.Controllers -{ - public class SqlController : Controller - { - [HttpPost] - public ActionResult Connection() - { - return RedirectToAction("Tables"); - } - - public ActionResult Index() - { - var constr = ConfigurationManager.ConnectionStrings["Db"]; - if (!string.IsNullOrEmpty(constr?.ConnectionString)) - ViewBag.ConnectionString = constr.ConnectionString ?? ""; - else - { - ViewBag.ConnectionString = ""; - ViewBag.NextLink = ""; - } - - - return View(); - } - - [HttpGet] - public ActionResult Tables() - { - ViewBag.GotException = false; - if (SetupTools.DbTools.GotUpToDateTables()) - ViewBag.GotTables = true; - else - { - ViewBag.GotTables = false; - ViewBag.NextLink = ""; - } - return View(); - } - - [HttpPost] - public ActionResult Tables(string go) - { - try - { - SetupTools.DbTools.CreateTables(); - SetupTools.DbTools.UpgradeDatabaseSchema(); - return Redirect(Url.GetNextWizardStep()); - } - catch (Exception ex) - { - ViewBag.GotException = true; - ViewBag.GotTables = false; - ModelState.AddModelError("", ex.Message); - ViewBag.FullException = ex.ToString(); - return View(); - } - //return RedirectToRoute(new {Area = "Installation", Controller = "Setup", Action = "Done"}); - } - - public ActionResult Validate() - { - try - { - var constr = ConfigurationManager.ConnectionStrings["Db"]; - SetupTools.DbTools.CheckConnectionString(constr?.ConnectionString); - return Content(@"{ ""result"": ""ok"" }", "application/json"); - } - catch (Exception ex) - { - var errMsg = ex.Message.Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\r", "") - .Replace("\n", "\\n"); - return Content(@"{ ""result"": ""fail"", ""reason"": """ + errMsg + @"""}", "application/json"); - } - } - - protected override void OnActionExecuting(ActionExecutingContext filterContext) - { - ViewBag.PrevLink = Url.GetPreviousWizardStepLink(); - ViewBag.NextLink = Url.GetNextWizardStepLink(); - base.OnActionExecuting(filterContext); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/InstallationAreaRegistration.cs b/src/Server/OneTrueError.Web/Areas/Installation/InstallationAreaRegistration.cs deleted file mode 100644 index 25997b55..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/InstallationAreaRegistration.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Configuration; -using System.Web.Mvc; - -namespace OneTrueError.Web.Areas.Installation -{ - public class InstallationAreaRegistration : AreaRegistration - { - public override string AreaName - { - get { return "Installation"; } - } - - public override void RegisterArea(AreaRegistrationContext context) - { - if (ConfigurationManager.AppSettings["Configured"] != "false") - return; - - context.MapRoute( - "Installation_default", - "Installation/{controller}/{action}/{id}", - new {action = "Index", controller = "Setup", id = UrlParameter.Optional} - ); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Models/AccountViewModel.cs b/src/Server/OneTrueError.Web/Areas/Installation/Models/AccountViewModel.cs deleted file mode 100644 index 630c2f11..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Models/AccountViewModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Installation.Models -{ - public class AccountViewModel - { - [Required, StringLength(255)] - public string EmailAddress { get; set; } - - [Required, StringLength(40)] - public string Password { get; set; } - - [Required, StringLength(40)] - public string UserName { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Models/BasicsViewModel.cs b/src/Server/OneTrueError.Web/Areas/Installation/Models/BasicsViewModel.cs deleted file mode 100644 index 303a87b3..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Models/BasicsViewModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Installation.Models -{ - public class BasicsViewModel - { - [Required, MinLength(4)] - public string BaseUrl { get; set; } - - [Required, EmailAddress] - public string SupportEmail { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Models/EmailViewModel.cs b/src/Server/OneTrueError.Web/Areas/Installation/Models/EmailViewModel.cs deleted file mode 100644 index f711b382..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Models/EmailViewModel.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Installation.Models -{ - public class EmailViewModel - { - [Display(Name = "Account Name")] - public string AccountName { get; set; } - - [Display(Name = "Account password")] - public string AccountPassword { get; set; } - - [Display(Name = "SMTP Port"), Required] - public int? PortNumber { get; set; } - - [Display(Name = "SMTP Host"), Required] - public string SmtpHost { get; set; } - - [Display(Name = "Use SSL")] - public bool UseSSL { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Models/ErrorTrackingViewModel.cs b/src/Server/OneTrueError.Web/Areas/Installation/Models/ErrorTrackingViewModel.cs deleted file mode 100644 index d61dd678..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Models/ErrorTrackingViewModel.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace OneTrueError.Web.Areas.Installation.Models -{ - public class ErrorTrackingViewModel - { - [Display(Name = "Activate tracking")] - public bool ActivateTracking { get; set; } - - [Display(Name = "Contact email"), EmailAddress] - public string ContactEmail { get; set; } - - /// - /// A fixed identity which identifies this specific installation. You can generate a GUID and then store it. - /// - /// - /// - /// Used to identify the number of installations that have the same issue. - /// - /// - public string InstallationId { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Models/QueueViewModel.cs b/src/Server/OneTrueError.Web/Areas/Installation/Models/QueueViewModel.cs deleted file mode 100644 index e48604cd..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Models/QueueViewModel.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace OneTrueError.Web.Areas.Installation.Models -{ - public class QueueViewModel - { - public bool EventAuthentication { get; set; } - - public string EventQueue { get; set; } - - public bool EventTransactions { get; set; } - - public bool FeedbackAuthentication { get; set; } - - public string FeedbackQueue { get; set; } - - public bool FeedbackTransactions { get; set; } - - public bool ReportAuthentication { get; set; } - public string ReportQueue { get; set; } - - public bool ReportTransactions { get; set; } - - public bool UseSql { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Account/Admin.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Account/Admin.cshtml deleted file mode 100644 index d45e4d11..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Account/Admin.cshtml +++ /dev/null @@ -1,70 +0,0 @@ -@model OneTrueError.Web.Areas.Installation.Models.AccountViewModel -@{ - ViewBag.Title = "Installation - Account"; -} -
-
- -

Account creation

-

You need to create an account to be able to login. You can at a later point invite other users to OneTrueError.

- @if (ViewBag.AlreadyCreated) - { -
- Account have already been created. -
- @Html.Raw(ViewBag.PrevLink) - @Html.Raw(ViewBag.NextLink) - } - else - { -
- @Html.ValidationSummary(false) -
- - -
-
- - -
-
- - -
-
- - -
-
-
- @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
- } - @if (ViewBag.Exception != null) - { -

Error

-
@ViewBag.Exception
- } -
-
- -@section scripts -{ - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Messaging/Email.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Messaging/Email.cshtml deleted file mode 100644 index dd034960..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Messaging/Email.cshtml +++ /dev/null @@ -1,56 +0,0 @@ -@model OneTrueError.Web.Areas.Installation.Models.EmailViewModel -@{ - ViewBag.Title = "Installation - Email configuration"; -} -
-
- -

Email configuration

-

- OneTrueError can send email notifcations upon different types of events (and password resets etc). To do this, we need - to have a SMTP account for mailing. -

-
- @Html.ValidationSummary(false) -
- @Html.LabelFor(x => x.SmtpHost, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.SmtpHost, new {@class = "form-control", placeholder = "IP Address or host name"}) - @Html.CheckBoxFor(x => x.UseSSL) Use SSL -
-
- @Html.LabelFor(x => x.PortNumber, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.PortNumber, new {@class = "form-control", placeholder = "Port number. Typically 25 for SMTP and 587 for ESMTPS. Consult your mail server configuration."}) -
-
- @Html.LabelFor(x => x.AccountName, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.AccountName, new {@class = "form-control", placeholder = "SMTP Server account name (empty = no authentication)"}) -
-
- @Html.LabelFor(x => x.AccountPassword, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.AccountPassword, new {@class = "form-control", olaceholder = "Password for the above account"}) -
-
- - -
-
-
- @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
-
-
-@section scripts -{ - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Basics.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Basics.cshtml deleted file mode 100644 index e0ff08cd..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Basics.cshtml +++ /dev/null @@ -1,33 +0,0 @@ -@model OneTrueError.Web.Areas.Installation.Models.BasicsViewModel -@{ - ViewBag.Title = "Installation - Basics"; -} -
-
- -

Base configuration

-

Let's start with the easy stuff.

-
- @Html.ValidationSummary(false) -
- - - Address used when visting this site. -
-
- - - Used by your users when they need support using OneTrueError, for instance for account troubles. Also used as sender in outbound emails. -
-
- @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Completed.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Completed.cshtml deleted file mode 100644 index f9cf10fb..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Completed.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@{ - ViewBag.Title = "Installation - Completed"; -} -
-
- -

Congratulations!

- -

The setup is now completed. You need to deactive this configuration part of OneTrueError for security reasons..

-

- Change the Configured appKey to "true" in your web.config. Then click "Complete" to start OneTrueError -

- @if (ViewBag.DisplayError) - { -

Error!

-
- Change the key in your web.config to this: <add key="Configured" value="true" />. OneTrueError won't start otherwise. -
- } -
- -
- @Html.Raw(ViewBag.PrevStep) -
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/ErrorTracking.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/ErrorTracking.cshtml deleted file mode 100644 index f69e2a0e..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/ErrorTracking.cshtml +++ /dev/null @@ -1,62 +0,0 @@ -@model OneTrueError.Web.Areas.Installation.Models.ErrorTrackingViewModel -@{ - ViewBag.Title = "Installation - Error tracking"; -} -
-
- -

Error tracking

-

- To correct bugs much faster we would like to activate OneTrueError for your installation. All exceptions - will be uploaded to our own installation for further analysis. -

-
- @Html.ValidationSummary(false) -
- @Html.CheckBoxFor(x => x.ActivateTracking, new {@class = "form-control", style = "display:inline;height:auto;width:inherit;"}) - @Html.LabelFor(x => x.ActivateTracking, new {@class = "control-label"}) -
- Allow us to track errors. -
-
- @Html.LabelFor(x => x.ContactEmail, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.ContactEmail, new {@class = "form-control", disabled = ""}) - Email address that we may contact if we need any further information (will also receive notifications when your errors have been corrected). -
-
- @Html.LabelFor(x => x.InstallationId, new {@class = "control-label"}) - @Html.TextBoxFor(x => x.InstallationId, new {@class = "form-control", disabled = ""}) - A fixed identity which identifies this specific installation. You can generate a GUID and then store it. Used to identify the number of installations that have the same issue. -
- A guid generated for your convencience if you want to enable this feature: @Guid.NewGuid().ToString("N") -
-
- @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
-
-
-@section scripts -{ - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Index.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Index.cshtml deleted file mode 100644 index f888bc5c..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Index.cshtml +++ /dev/null @@ -1,18 +0,0 @@ -@{ - ViewBag.Title = "Installation - First step"; -} -
-
- -

Welcome

- -

Welcome to the OneTrueError installation.

-

- This guide will install everything neccesary to get started. Most of the settings are stored in the Settings table in the database. -

- - - @Html.Raw(ViewBag.NextLink) -
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Support.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Support.cshtml deleted file mode 100644 index 2296a401..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Setup/Support.cshtml +++ /dev/null @@ -1,28 +0,0 @@ -@model OneTrueError.Web.Areas.Installation.Models.SupportViewModel -@{ - ViewBag.Title = "Installation - Support"; -} -
-
- -

Free Support

-

Do you want to get 30 days of free email support?

-

No strings attached. You can optionally extend the support after the 30 days.

-
- @Html.ValidationSummary(false) -
- - - -
-
(Leave fields empty if you do not want to sign up)
-
 
-
- @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
-
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Shared/_Layout.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Shared/_Layout.cshtml deleted file mode 100644 index ccd70fe5..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Shared/_Layout.cshtml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - @ViewBag.Title - OneTrueError - - @Styles.Render("~/Content/css") - @Scripts.Render("~/bundles/modernizr") - @RenderSection("Styles", false) - - - - - -
-
-
-
- @RenderBody() -
-
-
-
-
-
- -
-
-
-@Scripts.Render("~/bundles/jquery") -@Scripts.Render("~/bundles/bootstrap") -@RenderSection("scripts", false) - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Sql/Index.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Sql/Index.cshtml deleted file mode 100644 index 79eb63cc..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Sql/Index.cshtml +++ /dev/null @@ -1,74 +0,0 @@ -@{ - ViewBag.Title = "Installation - Database configuration"; -} -
-
-
-

Database configuration

- -

- It's time to to configure the database. To do that you need - to start by specifying which database to use. We expect that you have created - a database and configured an account for it. -

-

- Modify the connectionString named 'Db' in web.config. Click on 'Test Connection' to make sure that it works. -

-
-
-

- @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
-
-
- -
-
-

Limitation

-

- - Currently only Microsoft SQL Server 2012 and above is supported. Need any other DB? Feel free to Contribute - by taking the SqlServer class library and convert it to a library for your favorite DB engine. - -

- -

Example

-
Data Source=(localdb)\ProjectsV12;Initial Catalog=OneTrueError;Integrated Security=True;Connect Timeout=30;
- -

Tip!

-

- Do you want to give permissions to the IIS app pool? Add "IIS APPPOOL\YourAppPool" as the windows account in SQL Server Management Studio. -

-

- For instance IIS APPPOOL\DefaultAppPool. -

-
-
-
- -@section scripts -{ - - -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/Sql/Tables.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/Sql/Tables.cshtml deleted file mode 100644 index 4e2b6c3b..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/Sql/Tables.cshtml +++ /dev/null @@ -1,38 +0,0 @@ -@{ - ViewBag.Title = "Tables"; -} - -
-
-

Tables

- -

It's time to install all the tables. Keep your fingers crossed and press "Create Tables"

- - @if (ViewBag.GotTables) - { -
- Tables have already been added/updated. -
- @Html.Raw(ViewBag.PrevLink) - @Html.Raw(ViewBag.NextLink) - } - else - { -
- @Html.ValidationSummary(true) - - @Html.Raw(ViewBag.PrevLink) - - @Html.Raw(ViewBag.NextLink) -
- } - @if (ViewBag.GotException) - { -

Error details

-
-
@ViewBag.FullException
-
-
- } -
-
\ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/_ViewStart.cshtml b/src/Server/OneTrueError.Web/Areas/Installation/Views/_ViewStart.cshtml deleted file mode 100644 index de23aeb7..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/_ViewStart.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@{ - Layout = "~/Areas/Installation/Views/Shared/_Layout.cshtml"; -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/Views/web.config b/src/Server/OneTrueError.Web/Areas/Installation/Views/web.config deleted file mode 100644 index 0c563eda..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/Views/web.config +++ /dev/null @@ -1,43 +0,0 @@ - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/WizardStepInfo.cs b/src/Server/OneTrueError.Web/Areas/Installation/WizardStepInfo.cs deleted file mode 100644 index 7dab164a..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/WizardStepInfo.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Web.Mvc; - -namespace OneTrueError.Web.Areas.Installation -{ - public class WizardStepInfo - { - public WizardStepInfo(string name, string virtualPath) - { - Name = name; - VirtualPath = virtualPath; - } - - public string Name { get; set; } - - public string VirtualPath { get; set; } - - public bool IsForAbsolutePath(string currentPath, UrlHelper helper) - { - var myPath = helper.Content(VirtualPath).TrimEnd('/'); - currentPath = currentPath.TrimEnd('/'); - return myPath.Equals(currentPath, StringComparison.OrdinalIgnoreCase); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Installation/WizardSteps.cs b/src/Server/OneTrueError.Web/Areas/Installation/WizardSteps.cs deleted file mode 100644 index c12f9533..00000000 --- a/src/Server/OneTrueError.Web/Areas/Installation/WizardSteps.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Web.Mvc; - -namespace OneTrueError.Web.Areas.Installation -{ - public static class WizardSteps - { - private static readonly WizardStepInfo[] Steps = - { - new WizardStepInfo("Introduction", "~/installation"), - new WizardStepInfo("Configure database", "~/installation/sql/"), - new WizardStepInfo("Create tables", "~/installation/sql/tables/"), - new WizardStepInfo("Base configuration", "~/installation/setup/basics/"), - new WizardStepInfo("Error tracking", "~/installation/setup/errors/"), - new WizardStepInfo("Create admin account", "~/installation/account/admin/"), - new WizardStepInfo("Mail settings", "~/installation/messaging/email/"), - new WizardStepInfo("Support", "~/installation/setup/support"), - new WizardStepInfo("Completed", "~/installation/setup/completed/") - }; - - - public static string GetNextWizardStep(this UrlHelper urlHelper) - { - var index = FindCurrentIndex(urlHelper); - if (index == -1) - return null; - if (index < Steps.Length - 1) - index++; - - var step = Steps[index]; - return urlHelper.Content(step.VirtualPath); - } - - public static string GetNextWizardStepLink(this UrlHelper urlHelper) - { - var index = FindCurrentIndex(urlHelper); - if (index == -1) - return ""; - if (index < Steps.Length - 1) - index++; - - var step = Steps[index]; - return - $@"{step.Name} >>"; - } - - public static string GetPreviousWizardStepLink(this UrlHelper urlHelper) - { - var index = FindCurrentIndex(urlHelper); - if (index == -1) - return ""; - if (index > 0) - index--; - - var step = Steps[index]; - return - $@"<< {step.Name}"; - } - - private static int FindCurrentIndex(UrlHelper urlHelper) - { - var currentPath = urlHelper.RequestContext.HttpContext.Request.Url.AbsolutePath; - for (var i = 0; i < Steps.Length; i++) - { - if (Steps[i].IsForAbsolutePath(currentPath, urlHelper)) - return i; - } - - return -1; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Controllers/FeedbackController.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Controllers/FeedbackController.cs deleted file mode 100644 index 3b421d82..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Controllers/FeedbackController.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; -using System.Data.Common; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using System.Web.Http; -using Griffin.Data; -using log4net; -using Newtonsoft.Json; -using OneTrueError.Infrastructure; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.ReportAnalyzer.LibContracts; -using OneTrueError.Web.Areas.Receiver.Helpers; -using OneTrueError.Web.Areas.Receiver.Models; - -namespace OneTrueError.Web.Areas.Receiver.Controllers -{ - [AllowAnonymous] - public class FeedbackController : ApiController - { - private readonly ILog _logger = LogManager.GetLogger(typeof(FeedbackController)); - private readonly IMessageQueue _queue; - - public FeedbackController(IMessageQueueProvider queueProvider) - { - _queue = queueProvider.Open("FeedbackQueue"); - } - - [HttpPost, Route("receiver/report/{appKey}/feedback")] - public async Task SupplyFeedback(string appKey, string sig, FeedbackModel model) - { - try - { - int appId; - using (var connection = ConnectionFactory.Create()) - { - using (var cmd = (DbCommand) connection.CreateCommand()) - { - cmd.CommandText = "SELECT Id FROM Applications WHERE AppKey = @key"; - cmd.AddParameter("key", appKey); - appId = (int) await cmd.ExecuteScalarAsync(); - } - } - using (var transaction = _queue.BeginTransaction()) - { - var dto = new ReceivedFeedbackDTO - { - ApplicationId = appId, - Description = model.Description, - EmailAddress = model.EmailAddress, - ReceivedAtUtc = DateTime.UtcNow, - RemoteAddress = Request.GetClientIpAddress(), - ReportId = model.ReportId, - ReportVersion = "1" - }; - _queue.Write(appId, dto); - - transaction.Commit(); - } - } - catch (Exception ex) - { - _logger.Warn( - "Failed to submit feedback: " + JsonConvert.SerializeObject(new {appKey, model}), - ex); - } - - return new HttpResponseMessage(HttpStatusCode.NoContent); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Controllers/ReportController.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Controllers/ReportController.cs deleted file mode 100644 index 1b3aa079..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Controllers/ReportController.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using System.Web; -using System.Web.Http; -using log4net; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.Web.Areas.Receiver.Helpers; -using OneTrueError.Web.Areas.Receiver.Models; - -namespace OneTrueError.Web.Areas.Receiver.Controllers -{ - [AllowAnonymous] - public class ReportController : ApiController - { - private static readonly SamplingCounter _samplingCounter = new SamplingCounter(); - private readonly ILog _logger = LogManager.GetLogger(typeof(ReportController)); - private readonly IMessageQueueProvider _queueProvider; - - static ReportController() - { - _samplingCounter.Load(); - } - - public ReportController(IMessageQueueProvider queueProvider) - { - _queueProvider = queueProvider; - } - - [HttpGet, Route("receiver/report/")] - public HttpResponseMessage Index() - { - var content = new StringContent("Hello world 2"); - //content.Headers.Add("Content-Type", "text/plain"); - var resp = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = content - }; - return resp; - } - - [HttpPost, Route("receiver/report/{appKey}")] - public async Task Post(string appKey, string sig) - { - if (HttpContext.Current.Request.InputStream.Length > 20000000) - { - return await KillLargeReportAsync(appKey); - } - - if (!_samplingCounter.CanAccept(appKey)) - return Request.CreateResponse(HttpStatusCode.OK); - - - try - { - var buffer = new byte[HttpContext.Current.Request.InputStream.Length]; - HttpContext.Current.Request.InputStream.Read(buffer, 0, buffer.Length); - var handler = new SaveReportHandler(_queueProvider); - await handler.BuildReportAsync(appKey, sig, Request.GetClientIpAddress(), buffer); - return Request.CreateResponse(HttpStatusCode.OK); - } - catch (Exception exception) - { - _logger.Error("Failed to handle request from " + appKey + " / " + Request.GetClientIpAddress(), - exception); - return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, exception); - } - } -#pragma warning disable 1998 - private async Task KillLargeReportAsync(string appKey) -#pragma warning restore 1998 - { - _logger.Error(appKey + "Too large report: " + HttpContext.Current.Request.InputStream.Length + " from " + - Request.GetClientIpAddress()); - //TODO: notify - return Request.CreateResponse(HttpStatusCode.OK); - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/HttpRequestMessageExtensions.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/HttpRequestMessageExtensions.cs deleted file mode 100644 index 86c3bb0b..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/HttpRequestMessageExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System.Net.Http; - -namespace OneTrueError.Web.Areas.Receiver.Helpers -{ - public static class HttpRequestMessageExtensions - { - private const string HttpContext = "MS_HttpContext"; - - private const string RemoteEndpointMessage = - "System.ServiceModel.Channels.RemoteEndpointMessageProperty"; - - private const string OwinContext = "MS_OwinContext"; - - public static string GetClientIpAddress(this HttpRequestMessage request) - { - // Web-hosting. Needs reference to System.Web.dll - if (request.Properties.ContainsKey(HttpContext)) - { - dynamic ctx = request.Properties[HttpContext]; - if (ctx != null) - { - return ctx.Request.UserHostAddress; - } - } - - // Self-hosting. Needs reference to System.ServiceModel.dll. - if (request.Properties.ContainsKey(RemoteEndpointMessage)) - { - dynamic remoteEndpoint = request.Properties[RemoteEndpointMessage]; - if (remoteEndpoint != null) - { - return remoteEndpoint.Address; - } - } - - // Self-hosting using Owin. Needs reference to Microsoft.Owin.dll. - if (request.Properties.ContainsKey(OwinContext)) - { - dynamic owinContext = request.Properties[OwinContext]; - if (owinContext != null) - { - return owinContext.Request.RemoteIpAddress; - } - } - - return null; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/IncludeNonPublicMembersContractResolver.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/IncludeNonPublicMembersContractResolver.cs deleted file mode 100644 index beb182dc..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/IncludeNonPublicMembersContractResolver.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System.Reflection; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -namespace OneTrueError.Web.Areas.Receiver.Helpers -{ - public class IncludeNonPublicMembersContractResolver : DefaultContractResolver - { - //protected override List GetSerializableMembers(Type objectType) - //{ - // var members = base.GetSerializableMembers(objectType); - // return members.Where(m => !m.Name.EndsWith("k__BackingField")).ToList(); - //} - - protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) - { - //TODO: Maybe cache - var prop = base.CreateProperty(member, memberSerialization); - - if (!prop.Writable) - { - var property = member as PropertyInfo; - if (property != null) - { - var hasPrivateSetter = property.GetSetMethod(true) != null; - prop.Writable = hasPrivateSetter; - } - } - - return prop; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/ReportDecompressor.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/ReportDecompressor.cs deleted file mode 100644 index 575e64ca..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/ReportDecompressor.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; -using System.IO.Compression; -using System.Text; - -namespace OneTrueError.Web.Areas.Receiver.Helpers -{ - public class ReportDecompressor - { - /// - /// Deflate a compressed error report in JSON format - /// - /// Compressed JSON errorReport - /// JSON string decompressed - public string Deflate(byte[] errorReport) - { - var zipStream = new MemoryStream(errorReport); - using (var deflateStream = new MemoryStream()) - { - using (var decompressor = new GZipStream(zipStream, CompressionMode.Decompress)) - { - decompressor.CopyTo(deflateStream); - deflateStream.Position = 0; - var buffer = new byte[deflateStream.Length]; - deflateStream.Read(buffer, 0, (int) deflateStream.Length); - var strBuffer = Encoding.UTF8.GetString(buffer); - return strBuffer; - } - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/SamplingSettings.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/SamplingSettings.cs deleted file mode 100644 index 640bb691..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/SamplingSettings.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Threading; - -namespace OneTrueError.Web.Areas.Receiver.Helpers -{ - public class SamplingSetting - { - private int _currentCount; - public string AppKey { get; set; } - public int Count { get; set; } - - /// - /// Accept all until the given count is reached (then ignore one). false means ignore all but the given count - /// index. - /// - public bool Inclusive { get; set; } - - public bool CanAccept() - { - var value = Interlocked.Increment(ref _currentCount); - - if (Inclusive) - { - var canAccept = value < Count; - if (canAccept) - return true; - - _currentCount = 0; - return false; - } - - else - { - var canAccept = value >= Count; - if (!canAccept) - return false; - - _currentCount = 0; - return true; - } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/SaveReportHandler.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/SaveReportHandler.cs deleted file mode 100644 index 9a7ade6d..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Helpers/SaveReportHandler.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System; -using System.Data; -using System.Data.SqlClient; -using System.Linq; -using System.Threading.Tasks; -using System.Web; -using Griffin.Data; -using Griffin.Data.Mapper; -using log4net; -using Newtonsoft.Json; -using OneTrueError.Infrastructure; -using OneTrueError.Infrastructure.Queueing; -using OneTrueError.ReportAnalyzer.LibContracts; -using OneTrueError.Web.Areas.Receiver.Models; -using OneTrueError.Web.Areas.Receiver.ReportingApi; - -namespace OneTrueError.Web.Areas.Receiver.Helpers -{ - /// - /// Validates inbound report and store it in our internal queue for analysis. - /// - public class SaveReportHandler - { - private readonly ILog _logger = LogManager.GetLogger(typeof(SaveReportHandler)); - private readonly IMessageQueue _queue; - - /// - /// Creates a new instance of . - /// - /// provider - public SaveReportHandler(IMessageQueueProvider queueProvider) - { - if (queueProvider == null) throw new ArgumentNullException("queueProvider"); - _queue = queueProvider.Open("ReportQueue"); - } - - public async Task BuildReportAsync(string appKey, string signatureProvidedByTheClient, string remoteAddress, - byte[] reportBody) - { - Guid tempKey; - if (!Guid.TryParse(appKey, out tempKey)) - { - _logger.Warn("Incorrect appKeyFormat: " + appKey + " from " + remoteAddress); - throw new HttpException(400, "AppKey must be a valid GUID which '" + appKey + "' is not."); - } - - var application = await GetAppAsync(appKey); - if (application == null) - { - _logger.Warn("Unknown appKey: " + appKey + " from " + remoteAddress); - throw new HttpException(400, "AppKey was not found in the database. Key '" + appKey + "'."); - } - - if (!ReportValidator.ValidateBody(application.SharedSecret, signatureProvidedByTheClient, reportBody)) - { - await StoreInvalidReportAsync(appKey, signatureProvidedByTheClient, remoteAddress, reportBody); - throw new HttpException(403, - "You either specified the wrong SharedSecret, or someone tampered with the data."); - } - - var report = DeserializeBody(reportBody); - - //fix malconfigured clients - if (report.CreatedAtUtc > DateTime.UtcNow) - report.CreatedAtUtc = DateTime.UtcNow; - - var internalDto = new ReceivedReportDTO - { - ApplicationId = application.Id, - RemoteAddress = remoteAddress, - ContextCollections = report.ContextCollections.Select(ConvertCollection).ToArray(), - CreatedAtUtc = report.CreatedAtUtc, - DateReceivedUtc = DateTime.UtcNow, - Exception = ConvertException(report.Exception), - ReportId = report.ReportId, - ReportVersion = report.ReportVersion - }; - - await StoreReportAsync(internalDto); - } - - private static ReceivedReportContextInfo ConvertCollection(NewReportContextInfo arg) - { - return new ReceivedReportContextInfo(arg.Name, arg.Properties); - } - - private static ReceivedReportException ConvertException(NewReportException exception) - { - var ex = new ReceivedReportException - { - Name = exception.Name, - AssemblyName = exception.AssemblyName, - BaseClasses = exception.BaseClasses, - Everything = exception.Everything, - FullName = exception.FullName, - Message = exception.Message, - Namespace = exception.Namespace, - Properties = exception.Properties, - StackTrace = exception.StackTrace - }; - if (exception.InnerException != null) - ex.InnerException = ConvertException(exception.InnerException); - return ex; - } - - private NewReportDTO DeserializeBody(byte[] body) - { - var decompressor = new ReportDecompressor(); - var json = decompressor.Deflate(body); - - return JsonConvert.DeserializeObject(json, - new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.Objects, - ContractResolver = - new IncludeNonPublicMembersContractResolver() - }); - } - - private static async Task GetAppAsync(string appKey) - { - using (var con = ConnectionFactory.Create()) - { - using (var cmd = con.CreateDbCommand()) - { - cmd.CommandText = "SELECT Id, SharedSecret FROM Applications WHERE AppKey = @key"; - cmd.AddParameter("key", appKey); - using (var reader = await cmd.ExecuteReaderAsync()) - { - if (!await reader.ReadAsync()) - return null; - - return new AppInfo - { - Id = reader.GetInt32(0), - SharedSecret = reader.GetString(1) - }; - } - } - } - } - - private async Task StoreInvalidReportAsync(string appKey, string sig, string remoteAddress, byte[] reportBody) - { - try - { - using (var connection = ConnectionFactory.Create()) - { - //TODO: Make something generic. - using (var cmd = (SqlCommand) connection.CreateCommand()) - { - cmd.CommandText = - @"INSERT INTO InvalidReports(appkey, signature, reportbody, errormessage, createdatutc) - VALUES (@appkey, @signature, @reportbody, @errormessage, @createdatutc);"; - cmd.AddParameter("appKey", appKey); - cmd.AddParameter("signature", sig); - var p = cmd.CreateParameter(); - p.SqlDbType = SqlDbType.Image; - p.ParameterName = "reportbody"; - p.Value = reportBody; - cmd.Parameters.Add(p); - //cmd.AddParameter("reportbody", reportBody); - cmd.AddParameter("errormessage", "Failed to validate signature"); - cmd.AddParameter("createdatutc", DateTime.UtcNow); - await cmd.ExecuteNonQueryAsync(); - } - } - } - catch (Exception) - { - //TODO: LOG - } - } - -#pragma warning disable 1998 - private async Task StoreReportAsync(ReceivedReportDTO report) -#pragma warning restore 1998 - { - try - { - _queue.Write(report.ApplicationId, report); - } - catch (Exception ex) - { - _logger.Warn( - "Failed to StoreReport: " + JsonConvert.SerializeObject(new {model = report}), ex); - } - } - - private class AppInfo - { - public int Id { get; set; } - public string SharedSecret { get; set; } - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Models/FeedbackModel.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Models/FeedbackModel.cs deleted file mode 100644 index ec578b65..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Models/FeedbackModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace OneTrueError.Web.Areas.Receiver.Models -{ - public class FeedbackModel - { - public string Description { get; set; } - public string EmailAddress { get; set; } - public string ReportId { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Models/SamplingCounter.cs b/src/Server/OneTrueError.Web/Areas/Receiver/Models/SamplingCounter.cs deleted file mode 100644 index dd4168b0..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Models/SamplingCounter.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using OneTrueError.Web.Areas.Receiver.Helpers; - -namespace OneTrueError.Web.Areas.Receiver.Models -{ - public class SamplingCounter - { - private readonly List _settings = new List(); - - public bool CanAccept(string appKey) - { - if (string.IsNullOrEmpty(appKey)) - return false; - - var item = _settings.FirstOrDefault(x => x.AppKey.Equals(appKey)); - if (item == null) - return true; - - return item.CanAccept(); - } - - public void Load() - { - var path = GetFilePath(); - if (!File.Exists(path)) - return; - - var json = File.ReadAllText(path, Encoding.UTF8); - var items = JsonConvert.DeserializeObject>(json); - _settings.AddRange(items); - } - - public void Save() - { - var file = GetFilePath(); - var json = JsonConvert.SerializeObject(_settings.ToArray()); - File.WriteAllText(file, json, Encoding.UTF8); - } - - private static string GetFilePath() - { - var path = AppDomain.CurrentDomain.BaseDirectory; - //if (Debugger.IsAttached) - //path = Path.GetFullPath(path + "..\\.."); - - path = Path.Combine(path, "SamplingSettings.json"); - return path; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/NamespaceDoc.cs b/src/Server/OneTrueError.Web/Areas/Receiver/NamespaceDoc.cs deleted file mode 100644 index eaf2e1ef..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/NamespaceDoc.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Web.Areas.Receiver -{ - // This file is Generated by the tool MarkdownToNamespaceDoc. ReadMe.md is the master. - - /// - /// Report receiver - /// - /// - /// The main purpose of this area is to just RECEIVE reports. No analytics is made at all. This distinction is made to make sure that all reports are stored successfully, even if something else in the system is having trouble. - /// The reports are later picked up by the ReportAnalyzer class library and are processed in it. - ///
- /// This area should be considered to be stand alone and not related to anything else in the web site. It exists here just to make the installation experience easier. - /// - ///
- [CompilerGenerated] - class NamespaceDoc - { - } -} diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/ReadMe.md b/src/Server/OneTrueError.Web/Areas/Receiver/ReadMe.md deleted file mode 100644 index 0a7fd68b..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/ReadMe.md +++ /dev/null @@ -1,11 +0,0 @@ -Report receiver -================ - -The main purpose of this area is to just RECEIVE reports. No analytics is made at all. This distinction is made to make sure that all reports are stored successfully, even if something else in the system is having trouble. - -The reports are later picked up by the ReportAnalyzer class library and are processed in it. - ----- - -*This area should be considered to be stand alone and not related to anything else in the web site. It exists here just to make the installation experience easier.* - diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NamespaceDoc.cs b/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NamespaceDoc.cs deleted file mode 100644 index 85bd0aef..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NamespaceDoc.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Runtime.CompilerServices; - -namespace OneTrueError.Web.Areas.Receiver.ReportingApi -{ - /// - /// These classes is an exact match of the client library DTOs. - /// - /// - /// - /// Do not modify these when the client library changes (unless the change is backwards compatible), instead create - /// a new class - /// which the changes are applied to and name it with the same version number that is sent by the client. - /// - /// - /// In that way we can support multiple API versions. - /// - /// - [CompilerGenerated] - public class NamespaceDoc - { - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportContextInfo.cs b/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportContextInfo.cs deleted file mode 100644 index 638c6e29..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportContextInfo.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace OneTrueError.Web.Areas.Receiver.ReportingApi -{ - public class NewReportContextInfo - { - public NewReportContextInfo() - { - } - - public NewReportContextInfo(string name, Dictionary properties) - { - if (name == null) throw new ArgumentNullException(nameof(name)); - if (properties == null) throw new ArgumentNullException(nameof(properties)); - Name = name; - Properties = properties; - } - - - public string Name { get; set; } - - public Dictionary Properties { get; set; } - - public override string ToString() - { - return Name + " [" + string.Join(", ", - Properties.Select(x => x.Key + "=" + x.Value)) + "]"; - } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportDTO.cs b/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportDTO.cs deleted file mode 100644 index a430c762..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/ReportingApi/NewReportDTO.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; - -namespace OneTrueError.Web.Areas.Receiver.ReportingApi -{ - /// - /// Report as uploaded by the client API. - /// - public class NewReportDTO - { - /// - /// A collection of context information such as HTTP request information or computer hardware info. - /// - public NewReportContextInfo[] ContextCollections { get; set; } - - /// - /// Date specified at client side - /// - public DateTime CreatedAtUtc { get; set; } - - - /// - /// Exception which was caught. - /// - public NewReportException Exception { get; set; } - - public string RemoteAddress { get; set; } - - /// - /// Gets incident id (unique identifier used in communication with the customer to identify this error) - /// - public string ReportId { get; set; } - - /// - /// Version of the report - /// - public string ReportVersion { get; set; } - } -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Areas/Receiver/Views/web.config b/src/Server/OneTrueError.Web/Areas/Receiver/Views/web.config deleted file mode 100644 index 0c563eda..00000000 --- a/src/Server/OneTrueError.Web/Areas/Receiver/Views/web.config +++ /dev/null @@ -1,43 +0,0 @@ - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/Site.css b/src/Server/OneTrueError.Web/Content/Site.css deleted file mode 100644 index 3d8717d2..00000000 --- a/src/Server/OneTrueError.Web/Content/Site.css +++ /dev/null @@ -1,113 +0,0 @@ -body { - padding-bottom: 20px; - padding-top: 50px; -} - -/* Set padding to keep content from hitting the edges */ - -.body-content { - padding-left: 15px; - padding-right: 15px; -} - -/* Override the default bootstrap behavior where horizontal description lists - will truncate terms that are too long to fit in the left column -*/ - -.dl-horizontal dt { white-space: normal; } - -/* Set width on the form input elements since they're 100% wide by default */ - -input, -select, -textarea { max-width: 280px; } - -/* Custom container */ - -.full-width .container { - margin: 0 auto; - width: 100%; -} - -.full-width .body-content { margin-top: 50px; } - -.full-width .navbar-brand, -.full-width .navbar-nav > li > a { - padding-bottom: 20px; - padding-left: 30px; - padding-right: 30px; - padding-top: 20px; -} - -.full-width .navbar-brand, -.full-width .navbar-nav.settings > li > a { - padding-bottom: 20px; - padding-left: 10px; - padding-right: 10px; - padding-top: 20px; -} - -.full-width .navbar-nav > .active > a { opacity: .6; } - -.tile h3, -.tile h4, -.tile h2 { margin-top: 5px !important; } - -.sub-menu.navbar { - background-color: #337ab7; - color: #fff; - msargin-top: 10px; - padding-left: 20px; - padding-top: 10px; -} - -.sub-menu.navbar .navbar-nav .navbar-brand { - font-family: Arial, Helvetica, sans-serif; - font-size: 12px; -} - -.sub-menu.navbar .navbar-nav > li > a { color: #fff; } - -.sub-menu.navbar .navbar-nav > li > a:hover, -.sub-menu.navbar .navbar-nav > li > a:focus { - background-color: #2d6da3; - color: #f2f2f2; -} - -.sub-menu.navbar .navbar-nav > .active > a, -.sub-menu.navbar .navbar-nav > .active > a:hover, -.sub-menu.navbar .navbar-nav > .active > a:focus { - background-color: #285f8f; - color: #fff; -} - -.sub-menu.navbar .navbar-nav > .disabled > a, -.sub-menu.navbar .navbar-nav > .disabled > a:hover, -.sub-menu.navbar .navbar-nav > .disabled > a:focus { - background-color: #000000; - color: #999999; -} - -.vertical-spacer { margin-top: 30px; } - -.tile.tile-wide, -.tile.tile-double { - height: auto; - min-height: 150; -} - -.tag { - color: white; - opacity: 0.75; - padding: 0 4px; -} - -.tag:hover { - color: #eee; - opacity: 1; - text-decoration: none; -} - -.tag.nephritis { background: #27ae60; } - -#chart-legend span { font-family: sans-serif; } \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/Site.less b/src/Server/OneTrueError.Web/Content/Site.less deleted file mode 100644 index 0ff3f69f..00000000 --- a/src/Server/OneTrueError.Web/Content/Site.less +++ /dev/null @@ -1,130 +0,0 @@ -body { - padding-top: 50px; - padding-bottom: 20px; -} - -/* Set padding to keep content from hitting the edges */ -.body-content { - padding-left: 15px; - padding-right: 15px; -} - -/* Override the default bootstrap behavior where horizontal description lists - will truncate terms that are too long to fit in the left column -*/ -.dl-horizontal dt { - white-space: normal; -} - -/* Set width on the form input elements since they're 100% wide by default */ -input, -select, -textarea { - max-width: 280px; -} - -/* Custom container */ -.full-width { - .container { - margin: 0 auto; - width: 100%; - } - - .body-content { - margin-top: 50px; - } - - .navbar-brand, .navbar-nav > li > a { - padding-left: 30px; - padding-right: 30px; - padding-top: 20px; - padding-bottom: 20px; - } - - .navbar-brand, .navbar-nav.settings > li > a { - padding-left: 10px; - padding-right: 10px; - padding-top: 20px; - padding-bottom: 20px; - } - - .navbar-nav > .active > a { - opacity: .6; - } -} - -.tile h3, .tile h4, .tile h2 { - margin-top: 5px !important; -} - -@submenu-bg-color: #337ab7; -@submenu-color: #fff; - -.sub-menu.navbar { - msargin-top: 10px; - padding-top: 10px; - padding-left: 20px; - color: @submenu-color; - background-color: @submenu-bg-color; - - .navbar-nav { - .navbar-brand { - font-family: Arial, Helvetica, sans-serif; - font-size: 12px; - } - - > li > a { - color: @submenu-color; - - &:hover, - &:focus { - color: darken(@submenu-color, 5%); - background-color: darken(@submenu-bg-color, 5%); - } - } - - > .active > a { - &, - &:hover, - &:focus { - color: @submenu-color; - background-color: darken(@submenu-bg-color, 10%); - } - } - - > .disabled > a { - &, - &:hover, - &:focus { - color: darken(@submenu-color, 40%); - background-color: darken(@submenu-bg-color, 60%); - } - } - } -} - -.vertical-spacer { - margin-top: 30px; -} - -.tile.tile-wide, .tile.tile-double { - height: auto; - min-height: 150; -} - -.tag{ - padding: 0 4px; - color: white; - opacity: 0.75; -} -.tag:hover{ - color: #eee; - opacity: 1; - text-decoration: none; -} -.tag.nephritis{ - background: #27ae60; -} -#chart-legend span { - font-family: sans-serif; -} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/Site.min.css b/src/Server/OneTrueError.Web/Content/Site.min.css deleted file mode 100644 index b2bda335..00000000 --- a/src/Server/OneTrueError.Web/Content/Site.min.css +++ /dev/null @@ -1 +0,0 @@ -body{padding-top:50px;padding-bottom:20px;}.body-content{padding-left:15px;padding-right:15px;}.dl-horizontal dt{white-space:normal;}input,select,textarea{max-width:280px;}.full-width .container{margin:0 auto;width:100%;}.full-width .body-content{margin-top:50px;}.full-width .navbar-brand,.full-width .navbar-nav>li>a{padding-left:30px;padding-right:30px;padding-top:20px;padding-bottom:20px;}.full-width .navbar-brand,.full-width .navbar-nav.settings>li>a{padding-left:10px;padding-right:10px;padding-top:20px;padding-bottom:20px;}.full-width .navbar-nav>.active>a{opacity:.6;}.tile h3,.tile h4,.tile h2{margin-top:5px !important;}.sub-menu.navbar{msargin-top:10px;padding-top:10px;padding-left:20px;color:#fff;background-color:#337ab7;}.sub-menu.navbar .navbar-nav .navbar-brand{font-family:Arial,Helvetica,sans-serif;font-size:12px;}.sub-menu.navbar .navbar-nav>li>a{color:#fff;}.sub-menu.navbar .navbar-nav>li>a:hover,.sub-menu.navbar .navbar-nav>li>a:focus{color:#f2f2f2;background-color:#2d6da3;}.sub-menu.navbar .navbar-nav>.active>a,.sub-menu.navbar .navbar-nav>.active>a:hover,.sub-menu.navbar .navbar-nav>.active>a:focus{color:#fff;background-color:#285f8f;}.sub-menu.navbar .navbar-nav>.disabled>a,.sub-menu.navbar .navbar-nav>.disabled>a:hover,.sub-menu.navbar .navbar-nav>.disabled>a:focus{color:#999;background-color:#000;}.vertical-spacer{margin-top:30px;}.tile.tile-wide,.tile.tile-double{height:auto;min-height:150;}.tag{padding:0 4px;color:#fff;opacity:.75;}.tag:hover{color:#eee;opacity:1;text-decoration:none;}.tag.nephritis{background:#27ae60;}#chart-legend span{font-family:sans-serif;} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.css b/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.css deleted file mode 100644 index 6d4a5c62..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.css +++ /dev/null @@ -1,876 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer { - position: fixed; - z-index: 1981; - top: 0; - right: 0; - bottom: 0; - left: 0; - padding: 0; - margin: 0; - background-color: #252525; - opacity: .5; -} -.alertify .ajs-modal { - position: fixed; - top: 0; - right: 0; - left: 0; - bottom: 0; - padding: 0; - overflow-y: auto; - z-index: 1981; -} -.alertify .ajs-dialog { - position: relative; - margin: 5% auto; - min-height: 110px; - max-width: 500px; - padding: 24px 24px 0 24px; - outline: 0; - background-color: #fff; -} -.alertify .ajs-dialog.ajs-capture:before { - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - display: block; - z-index: 1; -} -.alertify .ajs-reset { - position: absolute !important; - display: inline !important; - width: 0 !important; - height: 0 !important; - opacity: 0 !important; -} -.alertify .ajs-commands { - position: absolute; - right: 4px; - margin: -14px 24px 0 0; - z-index: 2; -} -.alertify .ajs-commands button { - display: none; - width: 10px; - height: 10px; - margin-left: 10px; - padding: 10px; - border: 0; - background-color: transparent; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; -} -.alertify .ajs-commands button.ajs-close { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAh0lEQVQYlY2QsQ0EIQwEB9cBAR1CJUaI/gigDnwR6NBL/7/xWLNrZ2b8EwGotVpr7eOitWa1VjugiNB7R1UPrKrWe0dEAHBbXUqxMQbeewDmnHjvyTm7C3zDwAUd9c63YQdUVdu6EAJzzquz7HXvTiklt+H9DQFYaxFjvDqllFyMkbXWvfpXHjJrWFgdBq/hAAAAAElFTkSuQmCC); -} -.alertify .ajs-commands button.ajs-maximize { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAOUlEQVQYlWP8//8/AzGAhYGBgaG4uBiv6t7eXkYmooxjYGAgWiELsvHYFMCcRX2rSXcjoSBiJDbAAeD+EGu+8BZcAAAAAElFTkSuQmCC); -} -.alertify .ajs-header { - margin: -24px; - margin-bottom: 0; - padding: 16px 24px; - background-color: #fff; -} -.alertify .ajs-body { - min-height: 56px; -} -.alertify .ajs-body .ajs-content { - padding: 16px 24px 16px 16px; -} -.alertify .ajs-footer { - padding: 4px; - margin-left: -24px; - margin-right: -24px; - min-height: 43px; - background-color: #fff; -} -.alertify .ajs-footer .ajs-buttons.ajs-primary { - text-align: right; -} -.alertify .ajs-footer .ajs-buttons.ajs-primary .ajs-button { - margin: 4px; -} -.alertify .ajs-footer .ajs-buttons.ajs-auxiliary { - float: left; - clear: none; - text-align: left; -} -.alertify .ajs-footer .ajs-buttons.ajs-auxiliary .ajs-button { - margin: 4px; -} -.alertify .ajs-footer .ajs-buttons .ajs-button { - min-width: 88px; - min-height: 35px; -} -.alertify .ajs-handle { - position: absolute; - display: none; - width: 10px; - height: 10px; - right: 0; - bottom: 0; - z-index: 1; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMS8xNEDQYmMAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQ0lEQVQYlaXNMQoAIAxD0dT7H657l0KX3iJuUlBUNOsPPCGJm7VDp6ryeMxMuDsAQH7owW3pyn3RS26iKxERMLN3ugOaAkaL3sWVigAAAABJRU5ErkJggg==); - -webkit-transform: scaleX(1) /*rtl:scaleX(-1)*/; - transform: scaleX(1) /*rtl:scaleX(-1)*/; - cursor: se-resize; -} -.alertify.ajs-no-overflow .ajs-body .ajs-content { - overflow: hidden !important; -} -.alertify.ajs-no-padding.ajs-maximized .ajs-body .ajs-content { - left: 0; - right: 0; - padding: 0; -} -.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body { - margin-left: -24px; - margin-right: -24px; -} -.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body .ajs-content { - padding: 0; -} -.alertify.ajs-no-padding.ajs-resizable .ajs-body .ajs-content { - left: 0; - right: 0; -} -.alertify.ajs-maximizable .ajs-commands button.ajs-maximize, -.alertify.ajs-maximizable .ajs-commands button.ajs-restore { - display: inline-block; -} -.alertify.ajs-closable .ajs-commands button.ajs-close { - display: inline-block; -} -.alertify.ajs-maximized .ajs-dialog { - width: 100% !important; - height: 100% !important; - max-width: none !important; - margin: 0 auto !important; - top: 0 !important; - left: 0 !important; -} -.alertify.ajs-maximized.ajs-modeless .ajs-modal { - position: fixed !important; - min-height: 100% !important; - max-height: none !important; - margin: 0 !important; -} -.alertify.ajs-maximized .ajs-commands button.ajs-maximize { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAASklEQVQYlZWQ0QkAMQhDtXRincOZX78KVtrDCwgqJNEoIB3MPLj7lRUROlpyVXGzby6zWuY+kz6tj5sBMTMAyVV3/595RbOh3cAXsww1raeiOcoAAAAASUVORK5CYII=); -} -.alertify.ajs-resizable .ajs-dialog, -.alertify.ajs-maximized .ajs-dialog { - padding: 0; -} -.alertify.ajs-resizable .ajs-commands, -.alertify.ajs-maximized .ajs-commands { - margin: 14px 24px 0 0; -} -.alertify.ajs-resizable .ajs-header, -.alertify.ajs-maximized .ajs-header { - position: absolute; - top: 0; - left: 0; - right: 0; - margin: 0; - padding: 16px 24px; -} -.alertify.ajs-resizable .ajs-body, -.alertify.ajs-maximized .ajs-body { - min-height: 224px; - display: inline-block; -} -.alertify.ajs-resizable .ajs-body .ajs-content, -.alertify.ajs-maximized .ajs-body .ajs-content { - position: absolute; - top: 50px; - right: 24px; - bottom: 50px; - left: 24px; - overflow: auto; -} -.alertify.ajs-resizable .ajs-footer, -.alertify.ajs-maximized .ajs-footer { - position: absolute; - left: 0; - right: 0; - bottom: 0; - margin: 0; -} -.alertify.ajs-resizable:not(.ajs-maximized) .ajs-dialog { - min-width: 548px; -} -.alertify.ajs-resizable:not(.ajs-maximized) .ajs-handle { - display: block; -} -.alertify.ajs-movable:not(.ajs-maximized) .ajs-header { - cursor: move; -} -.alertify.ajs-modeless .ajs-dimmer, -.alertify.ajs-modeless .ajs-reset { - display: none; -} -.alertify.ajs-modeless .ajs-modal { - overflow: visible; - max-width: none; - max-height: 0; -} -.alertify.ajs-modeless.ajs-pinnable .ajs-commands button.ajs-pin { - display: inline-block; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQklEQVQYlcWPMQ4AIAwCqU9u38GbcbHRWN1MvKQDhQFMEpKImGJA0gCgnYw0V0rwxseg5erT4oSkQVI5d9f+e9+xA0NbLpWfitPXAAAAAElFTkSuQmCC); -} -.alertify.ajs-modeless.ajs-unpinned .ajs-modal { - position: absolute; -} -.alertify.ajs-modeless.ajs-unpinned .ajs-commands button.ajs-pin { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAO0lEQVQYlWP8//8/AzGAiShV6AqLi4txGs+CLoBLMYbC3t5eRmyaWfBZhwwYkX2NTxPRvibKjRhW4wMAhxkYGbLu3pEAAAAASUVORK5CYII=); -} -.alertify.ajs-modeless:not(.ajs-unpinned) .ajs-body { - max-height: 500px; - overflow: auto; -} -.alertify.ajs-basic .ajs-header { - opacity: 0; -} -.alertify.ajs-basic .ajs-footer { - visibility: hidden; -} -.alertify.ajs-frameless .ajs-header { - position: absolute; - top: 0; - left: 0; - right: 0; - min-height: 60px; - margin: 0; - padding: 0; - opacity: 0; - z-index: 1; -} -.alertify.ajs-frameless .ajs-footer { - display: none; -} -.alertify.ajs-frameless .ajs-body .ajs-content { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; -} -.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog { - padding-top: 0; -} -.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog .ajs-commands { - margin-top: 0; -} -.ajs-no-overflow { - overflow: hidden !important; - outline: none; -} -.ajs-no-selection, -.ajs-no-selection * { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -@media screen and (max-width: 568px) { - .alertify .ajs-dialog { - min-width: 150px; - } - .alertify:not(.ajs-maximized) .ajs-modal { - padding: 0 5%; - } - .alertify:not(.ajs-maximized).ajs-resizable .ajs-dialog { - min-width: initial; - min-width: auto /*IE fallback*/; - } -} -@-moz-document url-prefix() { - .alertify button:focus { - outline: 1px dotted #3593D2; - } -} -.alertify .ajs-dimmer, -.alertify .ajs-modal { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - transition-property: opacity, visibility; - transition-timing-function: linear; - transition-duration: 250ms; -} -.alertify.ajs-hidden .ajs-dimmer, -.alertify.ajs-hidden .ajs-modal { - visibility: hidden; - opacity: 0; -} -.alertify.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-duration: 500ms; - animation-duration: 500ms; -} -.alertify.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-duration: 250ms; - animation-duration: 250ms; -} -.alertify .ajs-dialog.ajs-shake { - -webkit-animation-name: ajs-shake; - animation-name: ajs-shake; - -webkit-animation-duration: .1s; - animation-duration: .1s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} -@-webkit-keyframes ajs-shake { - 0%, - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 10%, - 30%, - 50%, - 70%, - 90% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - 20%, - 40%, - 60%, - 80% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } -} -@keyframes ajs-shake { - 0%, - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 10%, - 30%, - 50%, - 70%, - 90% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } - 20%, - 40%, - 60%, - 80% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } -} -.alertify.ajs-slide.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-slideIn; - animation-name: ajs-slideIn; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); -} -.alertify.ajs-slide.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-slideOut; - animation-name: ajs-slideOut; - -webkit-animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); - animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); -} -.alertify.ajs-zoom.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-zoomIn; - animation-name: ajs-zoomIn; -} -.alertify.ajs-zoom.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-zoomOut; - animation-name: ajs-zoomOut; -} -.alertify.ajs-fade.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-fadeIn; - animation-name: ajs-fadeIn; -} -.alertify.ajs-fade.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-fadeOut; - animation-name: ajs-fadeOut; -} -.alertify.ajs-pulse.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-pulseIn; - animation-name: ajs-pulseIn; -} -.alertify.ajs-pulse.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-pulseOut; - animation-name: ajs-pulseOut; -} -.alertify.ajs-flipx.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-flipInX; - animation-name: ajs-flipInX; -} -.alertify.ajs-flipx.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-flipOutX; - animation-name: ajs-flipOutX; -} -.alertify.ajs-flipy.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-flipInY; - animation-name: ajs-flipInY; -} -.alertify.ajs-flipy.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-flipOutY; - animation-name: ajs-flipOutY; -} -@-webkit-keyframes ajs-pulseIn { - 0%, - 20%, - 40%, - 60%, - 80%, - 100% { - transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - 20% { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - 40% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - 60% { - opacity: 1; - -webkit-transform: scale3d(1.03, 1.03, 1.03); - transform: scale3d(1.03, 1.03, 1.03); - } - 80% { - -webkit-transform: scale3d(0.97, 0.97, 0.97); - transform: scale3d(0.97, 0.97, 0.97); - } - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} -@keyframes ajs-pulseIn { - 0%, - 20%, - 40%, - 60%, - 80%, - 100% { - transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - 20% { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - 40% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - 60% { - opacity: 1; - -webkit-transform: scale3d(1.03, 1.03, 1.03); - transform: scale3d(1.03, 1.03, 1.03); - } - 80% { - -webkit-transform: scale3d(0.97, 0.97, 0.97); - transform: scale3d(0.97, 0.97, 0.97); - } - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} -@-webkit-keyframes ajs-pulseOut { - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - 50%, - 55% { - opacity: 1; - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - 100% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } -} -@keyframes ajs-pulseOut { - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - 50%, - 55% { - opacity: 1; - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - 100% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } -} -@-webkit-keyframes ajs-zoomIn { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.25, 0.25, 0.25); - transform: scale3d(0.25, 0.25, 0.25); - } - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} -@keyframes ajs-zoomIn { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.25, 0.25, 0.25); - transform: scale3d(0.25, 0.25, 0.25); - } - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} -@-webkit-keyframes ajs-zoomOut { - 0% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - 100% { - opacity: 0; - -webkit-transform: scale3d(0.25, 0.25, 0.25); - transform: scale3d(0.25, 0.25, 0.25); - } -} -@keyframes ajs-zoomOut { - 0% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - 100% { - opacity: 0; - -webkit-transform: scale3d(0.25, 0.25, 0.25); - transform: scale3d(0.25, 0.25, 0.25); - } -} -@-webkit-keyframes ajs-fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@keyframes ajs-fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@-webkit-keyframes ajs-fadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} -@keyframes ajs-fadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} -@-webkit-keyframes ajs-flipInX { - 0% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transition-timing-function: ease-in; - opacity: 0; - } - 40% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transition-timing-function: ease-in; - } - 60% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - opacity: 1; - } - 80% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - } - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} -@keyframes ajs-flipInX { - 0% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transition-timing-function: ease-in; - opacity: 0; - } - 40% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transition-timing-function: ease-in; - } - 60% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - transform: perspective(400px) rotate3d(1, 0, 0, 10deg); - opacity: 1; - } - 80% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - transform: perspective(400px) rotate3d(1, 0, 0, -5deg); - } - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} -@-webkit-keyframes ajs-flipOutX { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - 30% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - opacity: 0; - } -} -@keyframes ajs-flipOutX { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - 30% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - transform: perspective(400px) rotate3d(1, 0, 0, -20deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - transform: perspective(400px) rotate3d(1, 0, 0, 90deg); - opacity: 0; - } -} -@-webkit-keyframes ajs-flipInY { - 0% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transition-timing-function: ease-in; - opacity: 0; - } - 40% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - transition-timing-function: ease-in; - } - 60% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - opacity: 1; - } - 80% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - } - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} -@keyframes ajs-flipInY { - 0% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transition-timing-function: ease-in; - opacity: 0; - } - 40% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - transform: perspective(400px) rotate3d(0, 1, 0, -20deg); - transition-timing-function: ease-in; - } - 60% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - transform: perspective(400px) rotate3d(0, 1, 0, 10deg); - opacity: 1; - } - 80% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - transform: perspective(400px) rotate3d(0, 1, 0, -5deg); - } - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} -@-webkit-keyframes ajs-flipOutY { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - 30% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - opacity: 0; - } -} -@keyframes ajs-flipOutY { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - 30% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - transform: perspective(400px) rotate3d(0, 1, 0, -15deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - transform: perspective(400px) rotate3d(0, 1, 0, 90deg); - opacity: 0; - } -} -@-webkit-keyframes ajs-slideIn { - 0% { - margin-top: -100%; - } - 100% { - margin-top: 5%; - } -} -@keyframes ajs-slideIn { - 0% { - margin-top: -100%; - } - 100% { - margin-top: 5%; - } -} -@-webkit-keyframes ajs-slideOut { - 0% { - margin-top: 5%; - } - 100% { - margin-top: -100%; - } -} -@keyframes ajs-slideOut { - 0% { - margin-top: 5%; - } - 100% { - margin-top: -100%; - } -} -.alertify-notifier { - position: fixed; - width: 0; - overflow: visible; - z-index: 1982; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); -} -.alertify-notifier .ajs-message { - position: relative; - width: 260px; - max-height: 0; - padding: 0; - opacity: 0; - margin: 0; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - transition-duration: 250ms; - transition-timing-function: linear; -} -.alertify-notifier .ajs-message.ajs-visible { - transition-duration: 500ms; - transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); - opacity: 1; - max-height: 100%; - padding: 15px; - margin-top: 10px; -} -.alertify-notifier .ajs-message.ajs-success { - background: rgba(91, 189, 114, 0.95); -} -.alertify-notifier .ajs-message.ajs-error { - background: rgba(217, 92, 92, 0.95); -} -.alertify-notifier .ajs-message.ajs-warning { - background: rgba(252, 248, 215, 0.95); -} -.alertify-notifier.ajs-top { - top: 10px; -} -.alertify-notifier.ajs-bottom { - bottom: 10px; -} -.alertify-notifier.ajs-right { - right: 10px; -} -.alertify-notifier.ajs-right .ajs-message { - right: -320px; -} -.alertify-notifier.ajs-right .ajs-message.ajs-visible { - right: 290px; -} -.alertify-notifier.ajs-left { - left: 10px; -} -.alertify-notifier.ajs-left .ajs-message { - left: -300px; -} -.alertify-notifier.ajs-left .ajs-message.ajs-visible { - left: 0; -} diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.min.css b/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.min.css deleted file mode 100644 index 64d241a7..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer,.alertify .ajs-modal{position:fixed;padding:0;z-index:1981;top:0;right:0;bottom:0;left:0}.alertify .ajs-dimmer{margin:0;background-color:#252525;opacity:.5}.alertify .ajs-modal{overflow-y:auto}.alertify .ajs-dialog{position:relative;margin:5% auto;min-height:110px;max-width:500px;padding:24px 24px 0;outline:0;background-color:#fff}.alertify .ajs-dialog.ajs-capture:before{content:'';position:absolute;top:0;right:0;bottom:0;left:0;display:block;z-index:1}.alertify .ajs-reset{position:absolute!important;display:inline!important;width:0!important;height:0!important;opacity:0!important}.alertify .ajs-commands{position:absolute;right:4px;margin:-14px 24px 0 0;z-index:2}.alertify .ajs-commands button{display:none;width:10px;height:10px;margin-left:10px;padding:10px;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.alertify .ajs-commands button.ajs-close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAh0lEQVQYlY2QsQ0EIQwEB9cBAR1CJUaI/gigDnwR6NBL/7/xWLNrZ2b8EwGotVpr7eOitWa1VjugiNB7R1UPrKrWe0dEAHBbXUqxMQbeewDmnHjvyTm7C3zDwAUd9c63YQdUVdu6EAJzzquz7HXvTiklt+H9DQFYaxFjvDqllFyMkbXWvfpXHjJrWFgdBq/hAAAAAElFTkSuQmCC)}.alertify .ajs-commands button.ajs-maximize{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAOUlEQVQYlWP8//8/AzGAhYGBgaG4uBiv6t7eXkYmooxjYGAgWiELsvHYFMCcRX2rSXcjoSBiJDbAAeD+EGu+8BZcAAAAAElFTkSuQmCC)}.alertify .ajs-header{margin:-24px -24px 0;padding:16px 24px;background-color:#fff}.alertify .ajs-body{min-height:56px}.alertify .ajs-body .ajs-content{padding:16px 24px 16px 16px}.alertify .ajs-footer{padding:4px;margin-left:-24px;margin-right:-24px;min-height:43px;background-color:#fff}.alertify.ajs-maximized .ajs-dialog,.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body .ajs-content,.alertify.ajs-resizable .ajs-dialog{padding:0}.alertify .ajs-footer .ajs-buttons.ajs-auxiliary .ajs-button,.alertify .ajs-footer .ajs-buttons.ajs-primary .ajs-button{margin:4px}.alertify .ajs-footer .ajs-buttons.ajs-primary{text-align:right}.alertify .ajs-footer .ajs-buttons.ajs-auxiliary{float:left;clear:none;text-align:left}.alertify .ajs-footer .ajs-buttons .ajs-button{min-width:88px;min-height:35px}.alertify .ajs-handle{position:absolute;display:none;width:10px;height:10px;right:0;bottom:0;z-index:1;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMS8xNEDQYmMAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQ0lEQVQYlaXNMQoAIAxD0dT7H657l0KX3iJuUlBUNOsPPCGJm7VDp6ryeMxMuDsAQH7owW3pyn3RS26iKxERMLN3ugOaAkaL3sWVigAAAABJRU5ErkJggg==);-webkit-transform:scaleX(1);transform:scaleX(1);cursor:se-resize}.alertify.ajs-no-overflow .ajs-body .ajs-content{overflow:hidden!important}.alertify.ajs-no-padding.ajs-maximized .ajs-body .ajs-content{left:0;right:0;padding:0}.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body{margin-left:-24px;margin-right:-24px}.alertify.ajs-no-padding.ajs-resizable .ajs-body .ajs-content{left:0;right:0}.alertify.ajs-closable .ajs-commands button.ajs-close,.alertify.ajs-maximizable .ajs-commands button.ajs-maximize,.alertify.ajs-maximizable .ajs-commands button.ajs-restore{display:inline-block}.alertify.ajs-maximized .ajs-dialog{width:100%!important;height:100%!important;max-width:none!important;margin:0 auto!important;top:0!important;left:0!important}.alertify.ajs-maximized.ajs-modeless .ajs-modal{position:fixed!important;min-height:100%!important;max-height:none!important;margin:0!important}.alertify.ajs-maximized .ajs-commands button.ajs-maximize{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAASklEQVQYlZWQ0QkAMQhDtXRincOZX78KVtrDCwgqJNEoIB3MPLj7lRUROlpyVXGzby6zWuY+kz6tj5sBMTMAyVV3/595RbOh3cAXsww1raeiOcoAAAAASUVORK5CYII=)}.alertify.ajs-maximized .ajs-commands,.alertify.ajs-resizable .ajs-commands{margin:14px 24px 0 0}.alertify.ajs-maximized .ajs-header,.alertify.ajs-resizable .ajs-header{position:absolute;top:0;left:0;right:0;margin:0;padding:16px 24px}.alertify.ajs-maximized .ajs-body,.alertify.ajs-resizable .ajs-body{min-height:224px;display:inline-block}.alertify.ajs-maximized .ajs-body .ajs-content,.alertify.ajs-resizable .ajs-body .ajs-content{position:absolute;top:50px;right:24px;bottom:50px;left:24px;overflow:auto}.alertify.ajs-maximized .ajs-footer,.alertify.ajs-resizable .ajs-footer{position:absolute;left:0;right:0;bottom:0;margin:0}.alertify.ajs-resizable:not(.ajs-maximized) .ajs-dialog{min-width:548px}.alertify.ajs-resizable:not(.ajs-maximized) .ajs-handle{display:block}.alertify.ajs-movable:not(.ajs-maximized) .ajs-header{cursor:move}.alertify.ajs-modeless .ajs-dimmer,.alertify.ajs-modeless .ajs-reset{display:none}.alertify.ajs-modeless .ajs-modal{overflow:visible;max-width:none;max-height:0}.alertify.ajs-modeless.ajs-pinnable .ajs-commands button.ajs-pin{display:inline-block;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQklEQVQYlcWPMQ4AIAwCqU9u38GbcbHRWN1MvKQDhQFMEpKImGJA0gCgnYw0V0rwxseg5erT4oSkQVI5d9f+e9+xA0NbLpWfitPXAAAAAElFTkSuQmCC)}.alertify.ajs-modeless.ajs-unpinned .ajs-modal{position:absolute}.alertify.ajs-modeless.ajs-unpinned .ajs-commands button.ajs-pin{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAO0lEQVQYlWP8//8/AzGAiShV6AqLi4txGs+CLoBLMYbC3t5eRmyaWfBZhwwYkX2NTxPRvibKjRhW4wMAhxkYGbLu3pEAAAAASUVORK5CYII=)}.alertify.ajs-modeless:not(.ajs-unpinned) .ajs-body{max-height:500px;overflow:auto}.alertify.ajs-basic .ajs-header{opacity:0}.alertify.ajs-basic .ajs-footer{visibility:hidden}.alertify.ajs-frameless .ajs-header{position:absolute;top:0;left:0;right:0;min-height:60px;margin:0;padding:0;opacity:0;z-index:1}.alertify.ajs-frameless .ajs-footer{display:none}.alertify.ajs-frameless .ajs-body .ajs-content{position:absolute;top:0;right:0;bottom:0;left:0}.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog{padding-top:0}.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog .ajs-commands{margin-top:0}.ajs-no-overflow{overflow:hidden!important;outline:0}.ajs-no-selection,.ajs-no-selection *{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@media screen and (max-width:568px){.alertify .ajs-dialog{min-width:150px}.alertify:not(.ajs-maximized) .ajs-modal{padding:0 5%}.alertify:not(.ajs-maximized).ajs-resizable .ajs-dialog{min-width:initial;min-width:auto}}@-moz-document url-prefix(){.alertify button:focus{outline:#3593D2 dotted 1px}}.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);transition-property:opacity,visibility;transition-timing-function:linear;transition-duration:250ms}.alertify.ajs-hidden .ajs-dimmer,.alertify.ajs-hidden .ajs-modal{visibility:hidden;opacity:0}.alertify.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-duration:.5s;animation-duration:.5s}.alertify.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-duration:250ms;animation-duration:250ms}.alertify .ajs-dialog.ajs-shake{-webkit-animation-name:ajs-shake;animation-name:ajs-shake;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}@-webkit-keyframes ajs-shake{0%,100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes ajs-shake{0%,100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.alertify.ajs-slide.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-slideIn;animation-name:ajs-slideIn;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1.275);animation-timing-function:cubic-bezier(.175,.885,.32,1.275)}.alertify.ajs-slide.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-slideOut;animation-name:ajs-slideOut;-webkit-animation-timing-function:cubic-bezier(.6,-.28,.735,.045);animation-timing-function:cubic-bezier(.6,-.28,.735,.045)}.alertify.ajs-zoom.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-zoomIn;animation-name:ajs-zoomIn}.alertify.ajs-zoom.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-zoomOut;animation-name:ajs-zoomOut}.alertify.ajs-fade.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-fadeIn;animation-name:ajs-fadeIn}.alertify.ajs-fade.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-fadeOut;animation-name:ajs-fadeOut}.alertify.ajs-pulse.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-pulseIn;animation-name:ajs-pulseIn}.alertify.ajs-pulse.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-pulseOut;animation-name:ajs-pulseOut}.alertify.ajs-flipx.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-flipInX;animation-name:ajs-flipInX}.alertify.ajs-flipx.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-flipOutX;animation-name:ajs-flipOutX}.alertify.ajs-flipy.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-flipInY;animation-name:ajs-flipInY}.alertify.ajs-flipy.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-flipOutY;animation-name:ajs-flipOutY}@-webkit-keyframes ajs-pulseIn{0%,100%,20%,40%,60%,80%{transition-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes ajs-pulseIn{0%,100%,20%,40%,60%,80%{transition-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes ajs-pulseOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes ajs-pulseOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@-webkit-keyframes ajs-zoomIn{0%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes ajs-zoomIn{0%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes ajs-zoomOut{0%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}100%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}}@keyframes ajs-zoomOut{0%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}100%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}}@-webkit-keyframes ajs-fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes ajs-fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes ajs-fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes ajs-fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes ajs-flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes ajs-flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,10deg);transform:perspective(400px) rotate3d(1,0,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-5deg);transform:perspective(400px) rotate3d(1,0,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes ajs-flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);opacity:0}}@keyframes ajs-flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-20deg);transform:perspective(400px) rotate3d(1,0,0,-20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,90deg);transform:perspective(400px) rotate3d(1,0,0,90deg);opacity:0}}@-webkit-keyframes ajs-flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-20deg);transform:perspective(400px) rotate3d(0,1,0,-20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,1,0,10deg);transform:perspective(400px) rotate3d(0,1,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-5deg);transform:perspective(400px) rotate3d(0,1,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes ajs-flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-20deg);transform:perspective(400px) rotate3d(0,1,0,-20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,1,0,10deg);transform:perspective(400px) rotate3d(0,1,0,10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-5deg);transform:perspective(400px) rotate3d(0,1,0,-5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes ajs-flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-15deg);transform:perspective(400px) rotate3d(0,1,0,-15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);opacity:0}}@keyframes ajs-flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,1,0,-15deg);transform:perspective(400px) rotate3d(0,1,0,-15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,1,0,90deg);transform:perspective(400px) rotate3d(0,1,0,90deg);opacity:0}}@-webkit-keyframes ajs-slideIn{0%{margin-top:-100%}100%{margin-top:5%}}@keyframes ajs-slideIn{0%{margin-top:-100%}100%{margin-top:5%}}@-webkit-keyframes ajs-slideOut{0%{margin-top:5%}100%{margin-top:-100%}}@keyframes ajs-slideOut{0%{margin-top:5%}100%{margin-top:-100%}}.alertify-notifier{position:fixed;width:0;overflow:visible;z-index:1982;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.alertify-notifier .ajs-message{position:relative;width:260px;max-height:0;padding:0;opacity:0;margin:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);transition-duration:250ms;transition-timing-function:linear}.alertify-notifier .ajs-message.ajs-visible{transition-duration:.5s;transition-timing-function:cubic-bezier(.175,.885,.32,1.275);opacity:1;max-height:100%;padding:15px;margin-top:10px}.alertify-notifier .ajs-message.ajs-success{background:rgba(91,189,114,.95)}.alertify-notifier .ajs-message.ajs-error{background:rgba(217,92,92,.95)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95)}.alertify-notifier.ajs-top{top:10px}.alertify-notifier.ajs-bottom{bottom:10px}.alertify-notifier.ajs-right{right:10px}.alertify-notifier.ajs-right .ajs-message{right:-320px}.alertify-notifier.ajs-right .ajs-message.ajs-visible{right:290px}.alertify-notifier.ajs-left{left:10px}.alertify-notifier.ajs-left .ajs-message{left:-300px}.alertify-notifier.ajs-left .ajs-message.ajs-visible{left:0} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.rtl.css b/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.rtl.css deleted file mode 100644 index b42d1ffd..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.rtl.css +++ /dev/null @@ -1,876 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer { - position: fixed; - z-index: 1981; - top: 0; - left: 0; - bottom: 0; - right: 0; - padding: 0; - margin: 0; - background-color: #252525; - opacity: .5; -} -.alertify .ajs-modal { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - padding: 0; - overflow-y: auto; - z-index: 1981; -} -.alertify .ajs-dialog { - position: relative; - margin: 5% auto; - min-height: 110px; - max-width: 500px; - padding: 24px 24px 0 24px; - outline: 0; - background-color: #fff; -} -.alertify .ajs-dialog.ajs-capture:before { - content: ''; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - display: block; - z-index: 1; -} -.alertify .ajs-reset { - position: absolute !important; - display: inline !important; - width: 0 !important; - height: 0 !important; - opacity: 0 !important; -} -.alertify .ajs-commands { - position: absolute; - left: 4px; - margin: -14px 0 0 24px; - z-index: 2; -} -.alertify .ajs-commands button { - display: none; - width: 10px; - height: 10px; - margin-right: 10px; - padding: 10px; - border: 0; - background-color: transparent; - background-repeat: no-repeat; - background-position: center; - cursor: pointer; -} -.alertify .ajs-commands button.ajs-close { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAh0lEQVQYlY2QsQ0EIQwEB9cBAR1CJUaI/gigDnwR6NBL/7/xWLNrZ2b8EwGotVpr7eOitWa1VjugiNB7R1UPrKrWe0dEAHBbXUqxMQbeewDmnHjvyTm7C3zDwAUd9c63YQdUVdu6EAJzzquz7HXvTiklt+H9DQFYaxFjvDqllFyMkbXWvfpXHjJrWFgdBq/hAAAAAElFTkSuQmCC); -} -.alertify .ajs-commands button.ajs-maximize { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAOUlEQVQYlWP8//8/AzGAhYGBgaG4uBiv6t7eXkYmooxjYGAgWiELsvHYFMCcRX2rSXcjoSBiJDbAAeD+EGu+8BZcAAAAAElFTkSuQmCC); -} -.alertify .ajs-header { - margin: -24px; - margin-bottom: 0; - padding: 16px 24px; - background-color: #fff; -} -.alertify .ajs-body { - min-height: 56px; -} -.alertify .ajs-body .ajs-content { - padding: 16px 16px 16px 24px; -} -.alertify .ajs-footer { - padding: 4px; - margin-right: -24px; - margin-left: -24px; - min-height: 43px; - background-color: #fff; -} -.alertify .ajs-footer .ajs-buttons.ajs-primary { - text-align: left; -} -.alertify .ajs-footer .ajs-buttons.ajs-primary .ajs-button { - margin: 4px; -} -.alertify .ajs-footer .ajs-buttons.ajs-auxiliary { - float: right; - clear: none; - text-align: right; -} -.alertify .ajs-footer .ajs-buttons.ajs-auxiliary .ajs-button { - margin: 4px; -} -.alertify .ajs-footer .ajs-buttons .ajs-button { - min-width: 88px; - min-height: 35px; -} -.alertify .ajs-handle { - position: absolute; - display: none; - width: 10px; - height: 10px; - left: 0; - bottom: 0; - z-index: 1; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMS8xNEDQYmMAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQ0lEQVQYlaXNMQoAIAxD0dT7H657l0KX3iJuUlBUNOsPPCGJm7VDp6ryeMxMuDsAQH7owW3pyn3RS26iKxERMLN3ugOaAkaL3sWVigAAAABJRU5ErkJggg==); - -webkit-transform: scaleX(-1); - transform: scaleX(-1); - cursor: sw-resize; -} -.alertify.ajs-no-overflow .ajs-body .ajs-content { - overflow: hidden !important; -} -.alertify.ajs-no-padding.ajs-maximized .ajs-body .ajs-content { - right: 0; - left: 0; - padding: 0; -} -.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body { - margin-right: -24px; - margin-left: -24px; -} -.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body .ajs-content { - padding: 0; -} -.alertify.ajs-no-padding.ajs-resizable .ajs-body .ajs-content { - right: 0; - left: 0; -} -.alertify.ajs-maximizable .ajs-commands button.ajs-maximize, -.alertify.ajs-maximizable .ajs-commands button.ajs-restore { - display: inline-block; -} -.alertify.ajs-closable .ajs-commands button.ajs-close { - display: inline-block; -} -.alertify.ajs-maximized .ajs-dialog { - width: 100% !important; - height: 100% !important; - max-width: none !important; - margin: 0 auto !important; - top: 0 !important; - right: 0 !important; -} -.alertify.ajs-maximized.ajs-modeless .ajs-modal { - position: fixed !important; - min-height: 100% !important; - max-height: none !important; - margin: 0 !important; -} -.alertify.ajs-maximized .ajs-commands button.ajs-maximize { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAASklEQVQYlZWQ0QkAMQhDtXRincOZX78KVtrDCwgqJNEoIB3MPLj7lRUROlpyVXGzby6zWuY+kz6tj5sBMTMAyVV3/595RbOh3cAXsww1raeiOcoAAAAASUVORK5CYII=); -} -.alertify.ajs-resizable .ajs-dialog, -.alertify.ajs-maximized .ajs-dialog { - padding: 0; -} -.alertify.ajs-resizable .ajs-commands, -.alertify.ajs-maximized .ajs-commands { - margin: 14px 0 0 24px; -} -.alertify.ajs-resizable .ajs-header, -.alertify.ajs-maximized .ajs-header { - position: absolute; - top: 0; - right: 0; - left: 0; - margin: 0; - padding: 16px 24px; -} -.alertify.ajs-resizable .ajs-body, -.alertify.ajs-maximized .ajs-body { - min-height: 224px; - display: inline-block; -} -.alertify.ajs-resizable .ajs-body .ajs-content, -.alertify.ajs-maximized .ajs-body .ajs-content { - position: absolute; - top: 50px; - left: 24px; - bottom: 50px; - right: 24px; - overflow: auto; -} -.alertify.ajs-resizable .ajs-footer, -.alertify.ajs-maximized .ajs-footer { - position: absolute; - right: 0; - left: 0; - bottom: 0; - margin: 0; -} -.alertify.ajs-resizable:not(.ajs-maximized) .ajs-dialog { - min-width: 548px; -} -.alertify.ajs-resizable:not(.ajs-maximized) .ajs-handle { - display: block; -} -.alertify.ajs-movable:not(.ajs-maximized) .ajs-header { - cursor: move; -} -.alertify.ajs-modeless .ajs-dimmer, -.alertify.ajs-modeless .ajs-reset { - display: none; -} -.alertify.ajs-modeless .ajs-modal { - overflow: visible; - max-width: none; - max-height: 0; -} -.alertify.ajs-modeless.ajs-pinnable .ajs-commands button.ajs-pin { - display: inline-block; - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQklEQVQYlcWPMQ4AIAwCqU9u38GbcbHRWN1MvKQDhQFMEpKImGJA0gCgnYw0V0rwxseg5erT4oSkQVI5d9f+e9+xA0NbLpWfitPXAAAAAElFTkSuQmCC); -} -.alertify.ajs-modeless.ajs-unpinned .ajs-modal { - position: absolute; -} -.alertify.ajs-modeless.ajs-unpinned .ajs-commands button.ajs-pin { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAO0lEQVQYlWP8//8/AzGAiShV6AqLi4txGs+CLoBLMYbC3t5eRmyaWfBZhwwYkX2NTxPRvibKjRhW4wMAhxkYGbLu3pEAAAAASUVORK5CYII=); -} -.alertify.ajs-modeless:not(.ajs-unpinned) .ajs-body { - max-height: 500px; - overflow: auto; -} -.alertify.ajs-basic .ajs-header { - opacity: 0; -} -.alertify.ajs-basic .ajs-footer { - visibility: hidden; -} -.alertify.ajs-frameless .ajs-header { - position: absolute; - top: 0; - right: 0; - left: 0; - min-height: 60px; - margin: 0; - padding: 0; - opacity: 0; - z-index: 1; -} -.alertify.ajs-frameless .ajs-footer { - display: none; -} -.alertify.ajs-frameless .ajs-body .ajs-content { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; -} -.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog { - padding-top: 0; -} -.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog .ajs-commands { - margin-top: 0; -} -.ajs-no-overflow { - overflow: hidden !important; - outline: none; -} -.ajs-no-selection, -.ajs-no-selection * { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; -} -@media screen and (max-width: 568px) { - .alertify .ajs-dialog { - min-width: 150px; - } - .alertify:not(.ajs-maximized) .ajs-modal { - padding: 0 5%; - } - .alertify:not(.ajs-maximized).ajs-resizable .ajs-dialog { - min-width: initial; - min-width: auto /*IE fallback*/; - } -} -@-moz-document url-prefix() { - .alertify button:focus { - outline: 1px dotted #3593D2; - } -} -.alertify .ajs-dimmer, -.alertify .ajs-modal { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - transition-property: opacity, visibility; - transition-timing-function: linear; - transition-duration: 250ms; -} -.alertify.ajs-hidden .ajs-dimmer, -.alertify.ajs-hidden .ajs-modal { - visibility: hidden; - opacity: 0; -} -.alertify.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-duration: 500ms; - animation-duration: 500ms; -} -.alertify.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-duration: 250ms; - animation-duration: 250ms; -} -.alertify .ajs-dialog.ajs-shake { - -webkit-animation-name: ajs-shake; - animation-name: ajs-shake; - -webkit-animation-duration: .1s; - animation-duration: .1s; - -webkit-animation-fill-mode: both; - animation-fill-mode: both; -} -@-webkit-keyframes ajs-shake { - 0%, - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 10%, - 30%, - 50%, - 70%, - 90% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } - 20%, - 40%, - 60%, - 80% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } -} -@keyframes ajs-shake { - 0%, - 100% { - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } - 10%, - 30%, - 50%, - 70%, - 90% { - -webkit-transform: translate3d(10px, 0, 0); - transform: translate3d(10px, 0, 0); - } - 20%, - 40%, - 60%, - 80% { - -webkit-transform: translate3d(-10px, 0, 0); - transform: translate3d(-10px, 0, 0); - } -} -.alertify.ajs-slide.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-slideIn; - animation-name: ajs-slideIn; - -webkit-animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); - animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); -} -.alertify.ajs-slide.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-slideOut; - animation-name: ajs-slideOut; - -webkit-animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); - animation-timing-function: cubic-bezier(0.6, -0.28, 0.735, 0.045); -} -.alertify.ajs-zoom.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-zoomIn; - animation-name: ajs-zoomIn; -} -.alertify.ajs-zoom.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-zoomOut; - animation-name: ajs-zoomOut; -} -.alertify.ajs-fade.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-fadeIn; - animation-name: ajs-fadeIn; -} -.alertify.ajs-fade.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-fadeOut; - animation-name: ajs-fadeOut; -} -.alertify.ajs-pulse.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-pulseIn; - animation-name: ajs-pulseIn; -} -.alertify.ajs-pulse.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-pulseOut; - animation-name: ajs-pulseOut; -} -.alertify.ajs-flipx.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-flipInX; - animation-name: ajs-flipInX; -} -.alertify.ajs-flipx.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-flipOutX; - animation-name: ajs-flipOutX; -} -.alertify.ajs-flipy.ajs-in:not(.ajs-hidden) .ajs-dialog { - -webkit-animation-name: ajs-flipInY; - animation-name: ajs-flipInY; -} -.alertify.ajs-flipy.ajs-out.ajs-hidden .ajs-dialog { - -webkit-animation-name: ajs-flipOutY; - animation-name: ajs-flipOutY; -} -@-webkit-keyframes ajs-pulseIn { - 0%, - 20%, - 40%, - 60%, - 80%, - 100% { - transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - 20% { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - 40% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - 60% { - opacity: 1; - -webkit-transform: scale3d(1.03, 1.03, 1.03); - transform: scale3d(1.03, 1.03, 1.03); - } - 80% { - -webkit-transform: scale3d(0.97, 0.97, 0.97); - transform: scale3d(0.97, 0.97, 0.97); - } - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} -@keyframes ajs-pulseIn { - 0%, - 20%, - 40%, - 60%, - 80%, - 100% { - transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); - } - 0% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } - 20% { - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - 40% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - 60% { - opacity: 1; - -webkit-transform: scale3d(1.03, 1.03, 1.03); - transform: scale3d(1.03, 1.03, 1.03); - } - 80% { - -webkit-transform: scale3d(0.97, 0.97, 0.97); - transform: scale3d(0.97, 0.97, 0.97); - } - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} -@-webkit-keyframes ajs-pulseOut { - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - 50%, - 55% { - opacity: 1; - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - 100% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } -} -@keyframes ajs-pulseOut { - 20% { - -webkit-transform: scale3d(0.9, 0.9, 0.9); - transform: scale3d(0.9, 0.9, 0.9); - } - 50%, - 55% { - opacity: 1; - -webkit-transform: scale3d(1.1, 1.1, 1.1); - transform: scale3d(1.1, 1.1, 1.1); - } - 100% { - opacity: 0; - -webkit-transform: scale3d(0.3, 0.3, 0.3); - transform: scale3d(0.3, 0.3, 0.3); - } -} -@-webkit-keyframes ajs-zoomIn { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.25, 0.25, 0.25); - transform: scale3d(0.25, 0.25, 0.25); - } - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} -@keyframes ajs-zoomIn { - 0% { - opacity: 0; - -webkit-transform: scale3d(0.25, 0.25, 0.25); - transform: scale3d(0.25, 0.25, 0.25); - } - 100% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } -} -@-webkit-keyframes ajs-zoomOut { - 0% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - 100% { - opacity: 0; - -webkit-transform: scale3d(0.25, 0.25, 0.25); - transform: scale3d(0.25, 0.25, 0.25); - } -} -@keyframes ajs-zoomOut { - 0% { - opacity: 1; - -webkit-transform: scale3d(1, 1, 1); - transform: scale3d(1, 1, 1); - } - 100% { - opacity: 0; - -webkit-transform: scale3d(0.25, 0.25, 0.25); - transform: scale3d(0.25, 0.25, 0.25); - } -} -@-webkit-keyframes ajs-fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@keyframes ajs-fadeIn { - 0% { - opacity: 0; - } - 100% { - opacity: 1; - } -} -@-webkit-keyframes ajs-fadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} -@keyframes ajs-fadeOut { - 0% { - opacity: 1; - } - 100% { - opacity: 0; - } -} -@-webkit-keyframes ajs-flipInX { - 0% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -90deg); - transform: perspective(400px) rotate3d(1, 0, 0, -90deg); - transition-timing-function: ease-in; - opacity: 0; - } - 40% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 20deg); - transform: perspective(400px) rotate3d(1, 0, 0, 20deg); - transition-timing-function: ease-in; - } - 60% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -10deg); - transform: perspective(400px) rotate3d(1, 0, 0, -10deg); - opacity: 1; - } - 80% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 5deg); - transform: perspective(400px) rotate3d(1, 0, 0, 5deg); - } - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} -@keyframes ajs-flipInX { - 0% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -90deg); - transform: perspective(400px) rotate3d(1, 0, 0, -90deg); - transition-timing-function: ease-in; - opacity: 0; - } - 40% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 20deg); - transform: perspective(400px) rotate3d(1, 0, 0, 20deg); - transition-timing-function: ease-in; - } - 60% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -10deg); - transform: perspective(400px) rotate3d(1, 0, 0, -10deg); - opacity: 1; - } - 80% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 5deg); - transform: perspective(400px) rotate3d(1, 0, 0, 5deg); - } - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} -@-webkit-keyframes ajs-flipOutX { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - 30% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 20deg); - transform: perspective(400px) rotate3d(1, 0, 0, 20deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -90deg); - transform: perspective(400px) rotate3d(1, 0, 0, -90deg); - opacity: 0; - } -} -@keyframes ajs-flipOutX { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - 30% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, 20deg); - transform: perspective(400px) rotate3d(1, 0, 0, 20deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotate3d(1, 0, 0, -90deg); - transform: perspective(400px) rotate3d(1, 0, 0, -90deg); - opacity: 0; - } -} -@-webkit-keyframes ajs-flipInY { - 0% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, -90deg); - transform: perspective(400px) rotate3d(0, -1, 0, -90deg); - transition-timing-function: ease-in; - opacity: 0; - } - 40% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, 20deg); - transform: perspective(400px) rotate3d(0, -1, 0, 20deg); - transition-timing-function: ease-in; - } - 60% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, -10deg); - transform: perspective(400px) rotate3d(0, -1, 0, -10deg); - opacity: 1; - } - 80% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, 5deg); - transform: perspective(400px) rotate3d(0, -1, 0, 5deg); - } - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} -@keyframes ajs-flipInY { - 0% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, -90deg); - transform: perspective(400px) rotate3d(0, -1, 0, -90deg); - transition-timing-function: ease-in; - opacity: 0; - } - 40% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, 20deg); - transform: perspective(400px) rotate3d(0, -1, 0, 20deg); - transition-timing-function: ease-in; - } - 60% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, -10deg); - transform: perspective(400px) rotate3d(0, -1, 0, -10deg); - opacity: 1; - } - 80% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, 5deg); - transform: perspective(400px) rotate3d(0, -1, 0, 5deg); - } - 100% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } -} -@-webkit-keyframes ajs-flipOutY { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - 30% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, 15deg); - transform: perspective(400px) rotate3d(0, -1, 0, 15deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, -90deg); - transform: perspective(400px) rotate3d(0, -1, 0, -90deg); - opacity: 0; - } -} -@keyframes ajs-flipOutY { - 0% { - -webkit-transform: perspective(400px); - transform: perspective(400px); - } - 30% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, 15deg); - transform: perspective(400px) rotate3d(0, -1, 0, 15deg); - opacity: 1; - } - 100% { - -webkit-transform: perspective(400px) rotate3d(0, -1, 0, -90deg); - transform: perspective(400px) rotate3d(0, -1, 0, -90deg); - opacity: 0; - } -} -@-webkit-keyframes ajs-slideIn { - 0% { - margin-top: -100%; - } - 100% { - margin-top: 5%; - } -} -@keyframes ajs-slideIn { - 0% { - margin-top: -100%; - } - 100% { - margin-top: 5%; - } -} -@-webkit-keyframes ajs-slideOut { - 0% { - margin-top: 5%; - } - 100% { - margin-top: -100%; - } -} -@keyframes ajs-slideOut { - 0% { - margin-top: 5%; - } - 100% { - margin-top: -100%; - } -} -.alertify-notifier { - position: fixed; - width: 0; - overflow: visible; - z-index: 1982; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); -} -.alertify-notifier .ajs-message { - position: relative; - width: 260px; - max-height: 0; - padding: 0; - opacity: 0; - margin: 0; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - transition-duration: 250ms; - transition-timing-function: linear; -} -.alertify-notifier .ajs-message.ajs-visible { - transition-duration: 500ms; - transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); - opacity: 1; - max-height: 100%; - padding: 15px; - margin-top: 10px; -} -.alertify-notifier .ajs-message.ajs-success { - background: rgba(91, 189, 114, 0.95); -} -.alertify-notifier .ajs-message.ajs-error { - background: rgba(217, 92, 92, 0.95); -} -.alertify-notifier .ajs-message.ajs-warning { - background: rgba(252, 248, 215, 0.95); -} -.alertify-notifier.ajs-top { - top: 10px; -} -.alertify-notifier.ajs-bottom { - bottom: 10px; -} -.alertify-notifier.ajs-right { - left: 10px; -} -.alertify-notifier.ajs-right .ajs-message { - left: -320px; -} -.alertify-notifier.ajs-right .ajs-message.ajs-visible { - left: 290px; -} -.alertify-notifier.ajs-left { - right: 10px; -} -.alertify-notifier.ajs-left .ajs-message { - right: -300px; -} -.alertify-notifier.ajs-left .ajs-message.ajs-visible { - right: 0; -} diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.rtl.min.css b/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.rtl.min.css deleted file mode 100644 index 1f648b77..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/alertify.rtl.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer,.alertify .ajs-modal{position:fixed;padding:0;z-index:1981;top:0;left:0;bottom:0;right:0}.alertify .ajs-dimmer{margin:0;background-color:#252525;opacity:.5}.alertify .ajs-modal{overflow-y:auto}.alertify .ajs-dialog{position:relative;margin:5% auto;min-height:110px;max-width:500px;padding:24px 24px 0;outline:0;background-color:#fff}.alertify .ajs-dialog.ajs-capture:before{content:'';position:absolute;top:0;left:0;bottom:0;right:0;display:block;z-index:1}.alertify .ajs-reset{position:absolute!important;display:inline!important;width:0!important;height:0!important;opacity:0!important}.alertify .ajs-commands{position:absolute;left:4px;margin:-14px 0 0 24px;z-index:2}.alertify .ajs-commands button{display:none;width:10px;height:10px;margin-right:10px;padding:10px;border:0;background-color:transparent;background-repeat:no-repeat;background-position:center;cursor:pointer}.alertify .ajs-commands button.ajs-close{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAh0lEQVQYlY2QsQ0EIQwEB9cBAR1CJUaI/gigDnwR6NBL/7/xWLNrZ2b8EwGotVpr7eOitWa1VjugiNB7R1UPrKrWe0dEAHBbXUqxMQbeewDmnHjvyTm7C3zDwAUd9c63YQdUVdu6EAJzzquz7HXvTiklt+H9DQFYaxFjvDqllFyMkbXWvfpXHjJrWFgdBq/hAAAAAElFTkSuQmCC)}.alertify .ajs-commands button.ajs-maximize{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAOUlEQVQYlWP8//8/AzGAhYGBgaG4uBiv6t7eXkYmooxjYGAgWiELsvHYFMCcRX2rSXcjoSBiJDbAAeD+EGu+8BZcAAAAAElFTkSuQmCC)}.alertify .ajs-header{margin:-24px -24px 0;padding:16px 24px;background-color:#fff}.alertify .ajs-body{min-height:56px}.alertify .ajs-body .ajs-content{padding:16px 16px 16px 24px}.alertify .ajs-footer{padding:4px;margin-right:-24px;margin-left:-24px;min-height:43px;background-color:#fff}.alertify.ajs-maximized .ajs-dialog,.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body .ajs-content,.alertify.ajs-resizable .ajs-dialog{padding:0}.alertify .ajs-footer .ajs-buttons.ajs-auxiliary .ajs-button,.alertify .ajs-footer .ajs-buttons.ajs-primary .ajs-button{margin:4px}.alertify .ajs-footer .ajs-buttons.ajs-primary{text-align:left}.alertify .ajs-footer .ajs-buttons.ajs-auxiliary{float:right;clear:none;text-align:right}.alertify .ajs-footer .ajs-buttons .ajs-button{min-width:88px;min-height:35px}.alertify .ajs-handle{position:absolute;display:none;width:10px;height:10px;left:0;bottom:0;z-index:1;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMS8xNEDQYmMAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQ0lEQVQYlaXNMQoAIAxD0dT7H657l0KX3iJuUlBUNOsPPCGJm7VDp6ryeMxMuDsAQH7owW3pyn3RS26iKxERMLN3ugOaAkaL3sWVigAAAABJRU5ErkJggg==);-webkit-transform:scaleX(-1);transform:scaleX(-1);cursor:sw-resize}.alertify.ajs-no-overflow .ajs-body .ajs-content{overflow:hidden!important}.alertify.ajs-no-padding.ajs-maximized .ajs-body .ajs-content{right:0;left:0;padding:0}.alertify.ajs-no-padding:not(.ajs-maximized) .ajs-body{margin-right:-24px;margin-left:-24px}.alertify.ajs-no-padding.ajs-resizable .ajs-body .ajs-content{right:0;left:0}.alertify.ajs-closable .ajs-commands button.ajs-close,.alertify.ajs-maximizable .ajs-commands button.ajs-maximize,.alertify.ajs-maximizable .ajs-commands button.ajs-restore{display:inline-block}.alertify.ajs-maximized .ajs-dialog{width:100%!important;height:100%!important;max-width:none!important;margin:0 auto!important;top:0!important;right:0!important}.alertify.ajs-maximized.ajs-modeless .ajs-modal{position:fixed!important;min-height:100%!important;max-height:none!important;margin:0!important}.alertify.ajs-maximized .ajs-commands button.ajs-maximize{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAASklEQVQYlZWQ0QkAMQhDtXRincOZX78KVtrDCwgqJNEoIB3MPLj7lRUROlpyVXGzby6zWuY+kz6tj5sBMTMAyVV3/595RbOh3cAXsww1raeiOcoAAAAASUVORK5CYII=)}.alertify.ajs-maximized .ajs-commands,.alertify.ajs-resizable .ajs-commands{margin:14px 0 0 24px}.alertify.ajs-maximized .ajs-header,.alertify.ajs-resizable .ajs-header{position:absolute;top:0;right:0;left:0;margin:0;padding:16px 24px}.alertify.ajs-maximized .ajs-body,.alertify.ajs-resizable .ajs-body{min-height:224px;display:inline-block}.alertify.ajs-maximized .ajs-body .ajs-content,.alertify.ajs-resizable .ajs-body .ajs-content{position:absolute;top:50px;left:24px;bottom:50px;right:24px;overflow:auto}.alertify.ajs-maximized .ajs-footer,.alertify.ajs-resizable .ajs-footer{position:absolute;right:0;left:0;bottom:0;margin:0}.alertify.ajs-resizable:not(.ajs-maximized) .ajs-dialog{min-width:548px}.alertify.ajs-resizable:not(.ajs-maximized) .ajs-handle{display:block}.alertify.ajs-movable:not(.ajs-maximized) .ajs-header{cursor:move}.alertify.ajs-modeless .ajs-dimmer,.alertify.ajs-modeless .ajs-reset{display:none}.alertify.ajs-modeless .ajs-modal{overflow:visible;max-width:none;max-height:0}.alertify.ajs-modeless.ajs-pinnable .ajs-commands button.ajs-pin{display:inline-block;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAQklEQVQYlcWPMQ4AIAwCqU9u38GbcbHRWN1MvKQDhQFMEpKImGJA0gCgnYw0V0rwxseg5erT4oSkQVI5d9f+e9+xA0NbLpWfitPXAAAAAElFTkSuQmCC)}.alertify.ajs-modeless.ajs-unpinned .ajs-modal{position:absolute}.alertify.ajs-modeless.ajs-unpinned .ajs-commands button.ajs-pin{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABZ0RVh0Q3JlYXRpb24gVGltZQAwNy8xMy8xNOrZqugAAAAcdEVYdFNvZnR3YXJlAEFkb2JlIEZpcmV3b3JrcyBDUzbovLKMAAAAO0lEQVQYlWP8//8/AzGAiShV6AqLi4txGs+CLoBLMYbC3t5eRmyaWfBZhwwYkX2NTxPRvibKjRhW4wMAhxkYGbLu3pEAAAAASUVORK5CYII=)}.alertify.ajs-modeless:not(.ajs-unpinned) .ajs-body{max-height:500px;overflow:auto}.alertify.ajs-basic .ajs-header{opacity:0}.alertify.ajs-basic .ajs-footer{visibility:hidden}.alertify.ajs-frameless .ajs-header{position:absolute;top:0;right:0;left:0;min-height:60px;margin:0;padding:0;opacity:0;z-index:1}.alertify.ajs-frameless .ajs-footer{display:none}.alertify.ajs-frameless .ajs-body .ajs-content{position:absolute;top:0;left:0;bottom:0;right:0}.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog{padding-top:0}.alertify.ajs-frameless:not(.ajs-resizable) .ajs-dialog .ajs-commands{margin-top:0}.ajs-no-overflow{overflow:hidden!important;outline:0}.ajs-no-selection,.ajs-no-selection *{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}@media screen and (max-width:568px){.alertify .ajs-dialog{min-width:150px}.alertify:not(.ajs-maximized) .ajs-modal{padding:0 5%}.alertify:not(.ajs-maximized).ajs-resizable .ajs-dialog{min-width:initial;min-width:auto}}@-moz-document url-prefix(){.alertify button:focus{outline:#3593D2 dotted 1px}}.alertify .ajs-dimmer,.alertify .ajs-modal{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);transition-property:opacity,visibility;transition-timing-function:linear;transition-duration:250ms}.alertify.ajs-hidden .ajs-dimmer,.alertify.ajs-hidden .ajs-modal{visibility:hidden;opacity:0}.alertify.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-duration:.5s;animation-duration:.5s}.alertify.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-duration:250ms;animation-duration:250ms}.alertify .ajs-dialog.ajs-shake{-webkit-animation-name:ajs-shake;animation-name:ajs-shake;-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-fill-mode:both;animation-fill-mode:both}@-webkit-keyframes ajs-shake{0%,100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}}@keyframes ajs-shake{0%,100%{-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}}.alertify.ajs-slide.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-slideIn;animation-name:ajs-slideIn;-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1.275);animation-timing-function:cubic-bezier(.175,.885,.32,1.275)}.alertify.ajs-slide.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-slideOut;animation-name:ajs-slideOut;-webkit-animation-timing-function:cubic-bezier(.6,-.28,.735,.045);animation-timing-function:cubic-bezier(.6,-.28,.735,.045)}.alertify.ajs-zoom.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-zoomIn;animation-name:ajs-zoomIn}.alertify.ajs-zoom.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-zoomOut;animation-name:ajs-zoomOut}.alertify.ajs-fade.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-fadeIn;animation-name:ajs-fadeIn}.alertify.ajs-fade.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-fadeOut;animation-name:ajs-fadeOut}.alertify.ajs-pulse.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-pulseIn;animation-name:ajs-pulseIn}.alertify.ajs-pulse.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-pulseOut;animation-name:ajs-pulseOut}.alertify.ajs-flipx.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-flipInX;animation-name:ajs-flipInX}.alertify.ajs-flipx.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-flipOutX;animation-name:ajs-flipOutX}.alertify.ajs-flipy.ajs-in:not(.ajs-hidden) .ajs-dialog{-webkit-animation-name:ajs-flipInY;animation-name:ajs-flipInY}.alertify.ajs-flipy.ajs-out.ajs-hidden .ajs-dialog{-webkit-animation-name:ajs-flipOutY;animation-name:ajs-flipOutY}@-webkit-keyframes ajs-pulseIn{0%,100%,20%,40%,60%,80%{transition-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes ajs-pulseIn{0%,100%,20%,40%,60%,80%{transition-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes ajs-pulseOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes ajs-pulseOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}100%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@-webkit-keyframes ajs-zoomIn{0%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@keyframes ajs-zoomIn{0%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}100%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}}@-webkit-keyframes ajs-zoomOut{0%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}100%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}}@keyframes ajs-zoomOut{0%{opacity:1;-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}100%{opacity:0;-webkit-transform:scale3d(.25,.25,.25);transform:scale3d(.25,.25,.25)}}@-webkit-keyframes ajs-fadeIn{0%{opacity:0}100%{opacity:1}}@keyframes ajs-fadeIn{0%{opacity:0}100%{opacity:1}}@-webkit-keyframes ajs-fadeOut{0%{opacity:1}100%{opacity:0}}@keyframes ajs-fadeOut{0%{opacity:1}100%{opacity:0}}@-webkit-keyframes ajs-flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-90deg);transform:perspective(400px) rotate3d(1,0,0,-90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,20deg);transform:perspective(400px) rotate3d(1,0,0,20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-10deg);transform:perspective(400px) rotate3d(1,0,0,-10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,5deg);transform:perspective(400px) rotate3d(1,0,0,5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes ajs-flipInX{0%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-90deg);transform:perspective(400px) rotate3d(1,0,0,-90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(1,0,0,20deg);transform:perspective(400px) rotate3d(1,0,0,20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-10deg);transform:perspective(400px) rotate3d(1,0,0,-10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(1,0,0,5deg);transform:perspective(400px) rotate3d(1,0,0,5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes ajs-flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,20deg);transform:perspective(400px) rotate3d(1,0,0,20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-90deg);transform:perspective(400px) rotate3d(1,0,0,-90deg);opacity:0}}@keyframes ajs-flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(1,0,0,20deg);transform:perspective(400px) rotate3d(1,0,0,20deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(1,0,0,-90deg);transform:perspective(400px) rotate3d(1,0,0,-90deg);opacity:0}}@-webkit-keyframes ajs-flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,-90deg);transform:perspective(400px) rotate3d(0,-1,0,-90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,20deg);transform:perspective(400px) rotate3d(0,-1,0,20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,-10deg);transform:perspective(400px) rotate3d(0,-1,0,-10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,5deg);transform:perspective(400px) rotate3d(0,-1,0,5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes ajs-flipInY{0%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,-90deg);transform:perspective(400px) rotate3d(0,-1,0,-90deg);transition-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,20deg);transform:perspective(400px) rotate3d(0,-1,0,20deg);transition-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,-10deg);transform:perspective(400px) rotate3d(0,-1,0,-10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,5deg);transform:perspective(400px) rotate3d(0,-1,0,5deg)}100%{-webkit-transform:perspective(400px);transform:perspective(400px)}}@-webkit-keyframes ajs-flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,15deg);transform:perspective(400px) rotate3d(0,-1,0,15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,-90deg);transform:perspective(400px) rotate3d(0,-1,0,-90deg);opacity:0}}@keyframes ajs-flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,15deg);transform:perspective(400px) rotate3d(0,-1,0,15deg);opacity:1}100%{-webkit-transform:perspective(400px) rotate3d(0,-1,0,-90deg);transform:perspective(400px) rotate3d(0,-1,0,-90deg);opacity:0}}@-webkit-keyframes ajs-slideIn{0%{margin-top:-100%}100%{margin-top:5%}}@keyframes ajs-slideIn{0%{margin-top:-100%}100%{margin-top:5%}}@-webkit-keyframes ajs-slideOut{0%{margin-top:5%}100%{margin-top:-100%}}@keyframes ajs-slideOut{0%{margin-top:5%}100%{margin-top:-100%}}.alertify-notifier{position:fixed;width:0;overflow:visible;z-index:1982;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0)}.alertify-notifier .ajs-message{position:relative;width:260px;max-height:0;padding:0;opacity:0;margin:0;-webkit-transform:translate3d(0,0,0);transform:translate3d(0,0,0);transition-duration:250ms;transition-timing-function:linear}.alertify-notifier .ajs-message.ajs-visible{transition-duration:.5s;transition-timing-function:cubic-bezier(.175,.885,.32,1.275);opacity:1;max-height:100%;padding:15px;margin-top:10px}.alertify-notifier .ajs-message.ajs-success{background:rgba(91,189,114,.95)}.alertify-notifier .ajs-message.ajs-error{background:rgba(217,92,92,.95)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95)}.alertify-notifier.ajs-top{top:10px}.alertify-notifier.ajs-bottom{bottom:10px}.alertify-notifier.ajs-right{left:10px}.alertify-notifier.ajs-right .ajs-message{left:-320px}.alertify-notifier.ajs-right .ajs-message.ajs-visible{left:290px}.alertify-notifier.ajs-left{right:10px}.alertify-notifier.ajs-left .ajs-message{right:-300px}.alertify-notifier.ajs-left .ajs-message.ajs-visible{right:0} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.css deleted file mode 100644 index 3edba6eb..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.css +++ /dev/null @@ -1,60 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer { - background-color: #000; - opacity: .5; -} -.alertify .ajs-dialog { - max-width: 600px; - min-height: 122px; - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - border-radius: 6px; -} -.alertify .ajs-header { - color: #333; - border-bottom: 1px solid #e5e5e5; - border-radius: 6px 6px 0 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 18px; -} -.alertify .ajs-body { - font-family: 'Roboto', sans-serif; - color: black; -} -.alertify.ajs-resizable .ajs-content, -.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content { - top: 58px; - bottom: 68px; -} -.alertify .ajs-footer { - background-color: #fff; - padding: 15px; - border-top: 1px solid #e5e5e5; - border-radius: 0 0 6px 6px; -} -.alertify-notifier .ajs-message { - background: rgba(255, 255, 255, 0.95); - color: #000; - text-align: center; - border: solid 1px #ddd; - border-radius: 2px; -} -.alertify-notifier .ajs-message.ajs-success { - color: #fff; - background: rgba(91, 189, 114, 0.95); - text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-error { - color: #fff; - background: rgba(217, 92, 92, 0.95); - text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-warning { - background: rgba(252, 248, 215, 0.95); - border-color: #999; -} diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.min.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.min.css deleted file mode 100644 index cb47bed3..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer{background-color:#000;opacity:.5}.alertify .ajs-dialog{max-width:600px;min-height:122px;background-color:#fff;border:1px solid rgba(0,0,0,.2);box-shadow:0 5px 15px rgba(0,0,0,.5);border-radius:6px}.alertify .ajs-header{color:#333;border-bottom:1px solid #e5e5e5;border-radius:6px 6px 0 0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:18px}.alertify .ajs-body{font-family:Roboto,sans-serif;color:#000}.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content,.alertify.ajs-resizable .ajs-content{top:58px;bottom:68px}.alertify .ajs-footer{background-color:#fff;padding:15px;border-top:1px solid #e5e5e5;border-radius:0 0 6px 6px}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:1px solid #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.rtl.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.rtl.css deleted file mode 100644 index 09cf61ec..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.rtl.css +++ /dev/null @@ -1,60 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer { - background-color: #000; - opacity: .5; -} -.alertify .ajs-dialog { - max-width: 600px; - min-height: 122px; - background-color: #fff; - border: 1px solid rgba(0, 0, 0, 0.2); - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); - border-radius: 6px; -} -.alertify .ajs-header { - color: #333; - border-bottom: 1px solid #e5e5e5; - border-radius: 6px 6px 0 0; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 18px; -} -.alertify .ajs-body { - font-family: 'Roboto', sans-serif; - color: black; -} -.alertify.ajs-resizable .ajs-content, -.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content { - top: 58px; - bottom: 68px; -} -.alertify .ajs-footer { - background-color: #fff; - padding: 15px; - border-top: 1px solid #e5e5e5; - border-radius: 0 0 6px 6px; -} -.alertify-notifier .ajs-message { - background: rgba(255, 255, 255, 0.95); - color: #000; - text-align: center; - border: solid 1px #ddd; - border-radius: 2px; -} -.alertify-notifier .ajs-message.ajs-success { - color: #fff; - background: rgba(91, 189, 114, 0.95); - text-shadow: 1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-error { - color: #fff; - background: rgba(217, 92, 92, 0.95); - text-shadow: 1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-warning { - background: rgba(252, 248, 215, 0.95); - border-color: #999; -} diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.rtl.min.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.rtl.min.css deleted file mode 100644 index 10e73834..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/bootstrap.rtl.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer{background-color:#000;opacity:.5}.alertify .ajs-dialog{max-width:600px;min-height:122px;background-color:#fff;border:1px solid rgba(0,0,0,.2);box-shadow:0 5px 15px rgba(0,0,0,.5);border-radius:6px}.alertify .ajs-header{color:#333;border-bottom:1px solid #e5e5e5;border-radius:6px 6px 0 0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:18px}.alertify .ajs-body{font-family:Roboto,sans-serif;color:#000}.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content,.alertify.ajs-resizable .ajs-content{top:58px;bottom:68px}.alertify .ajs-footer{background-color:#fff;padding:15px;border-top:1px solid #e5e5e5;border-radius:0 0 6px 6px}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:1px solid #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.css deleted file mode 100644 index afd58bd5..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.css +++ /dev/null @@ -1,68 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dialog { - background-color: white; - box-shadow: 0px 15px 20px 0px rgba(0, 0, 0, 0.25); - border-radius: 2px; -} -.alertify .ajs-header { - color: black; - font-weight: bold; - background: #fafafa; - border-bottom: #eee 1px solid; - border-radius: 2px 2px 0 0; -} -.alertify .ajs-body { - color: black; -} -.alertify .ajs-body .ajs-content .ajs-input { - display: block; - width: 100%; - padding: 8px; - margin: 4px; - border-radius: 2px; - border: 1px solid #CCC; -} -.alertify .ajs-body .ajs-content p { - margin: 0; -} -.alertify .ajs-footer { - background: #fbfbfb; - border-top: #eee 1px solid; - border-radius: 0 0 2px 2px; -} -.alertify .ajs-footer .ajs-buttons .ajs-button { - background-color: transparent; - color: #000; - border: 0; - font-size: 14px; - font-weight: bold; - text-transform: uppercase; -} -.alertify .ajs-footer .ajs-buttons .ajs-button.ajs-ok { - color: #3593D2; -} -.alertify-notifier .ajs-message { - background: rgba(255, 255, 255, 0.95); - color: #000; - text-align: center; - border: solid 1px #ddd; - border-radius: 2px; -} -.alertify-notifier .ajs-message.ajs-success { - color: #fff; - background: rgba(91, 189, 114, 0.95); - text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-error { - color: #fff; - background: rgba(217, 92, 92, 0.95); - text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-warning { - background: rgba(252, 248, 215, 0.95); - border-color: #999; -} diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.min.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.min.css deleted file mode 100644 index 38193ca7..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dialog{background-color:#fff;box-shadow:0 15px 20px 0 rgba(0,0,0,.25);border-radius:2px}.alertify .ajs-header{color:#000;font-weight:700;background:#fafafa;border-bottom:#eee 1px solid;border-radius:2px 2px 0 0}.alertify .ajs-body{color:#000}.alertify .ajs-body .ajs-content .ajs-input{display:block;width:100%;padding:8px;margin:4px;border-radius:2px;border:1px solid #CCC}.alertify .ajs-body .ajs-content p{margin:0}.alertify .ajs-footer{background:#fbfbfb;border-top:#eee 1px solid;border-radius:0 0 2px 2px}.alertify .ajs-footer .ajs-buttons .ajs-button{background-color:transparent;color:#000;border:0;font-size:14px;font-weight:700;text-transform:uppercase}.alertify .ajs-footer .ajs-buttons .ajs-button.ajs-ok{color:#3593D2}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:1px solid #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.rtl.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.rtl.css deleted file mode 100644 index 907ca315..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.rtl.css +++ /dev/null @@ -1,68 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dialog { - background-color: white; - box-shadow: 0px 15px 20px 0px rgba(0, 0, 0, 0.25); - border-radius: 2px; -} -.alertify .ajs-header { - color: black; - font-weight: bold; - background: #fafafa; - border-bottom: #eee 1px solid; - border-radius: 2px 2px 0 0; -} -.alertify .ajs-body { - color: black; -} -.alertify .ajs-body .ajs-content .ajs-input { - display: block; - width: 100%; - padding: 8px; - margin: 4px; - border-radius: 2px; - border: 1px solid #CCC; -} -.alertify .ajs-body .ajs-content p { - margin: 0; -} -.alertify .ajs-footer { - background: #fbfbfb; - border-top: #eee 1px solid; - border-radius: 0 0 2px 2px; -} -.alertify .ajs-footer .ajs-buttons .ajs-button { - background-color: transparent; - color: #000; - border: 0; - font-size: 14px; - font-weight: bold; - text-transform: uppercase; -} -.alertify .ajs-footer .ajs-buttons .ajs-button.ajs-ok { - color: #3593D2; -} -.alertify-notifier .ajs-message { - background: rgba(255, 255, 255, 0.95); - color: #000; - text-align: center; - border: solid 1px #ddd; - border-radius: 2px; -} -.alertify-notifier .ajs-message.ajs-success { - color: #fff; - background: rgba(91, 189, 114, 0.95); - text-shadow: 1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-error { - color: #fff; - background: rgba(217, 92, 92, 0.95); - text-shadow: 1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-warning { - background: rgba(252, 248, 215, 0.95); - border-color: #999; -} diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.rtl.min.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.rtl.min.css deleted file mode 100644 index 1d420f5e..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/default.rtl.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dialog{background-color:#fff;box-shadow:0 15px 20px 0 rgba(0,0,0,.25);border-radius:2px}.alertify .ajs-header{color:#000;font-weight:700;background:#fafafa;border-bottom:#eee 1px solid;border-radius:2px 2px 0 0}.alertify .ajs-body{color:#000}.alertify .ajs-body .ajs-content .ajs-input{display:block;width:100%;padding:8px;margin:4px;border-radius:2px;border:1px solid #CCC}.alertify .ajs-body .ajs-content p{margin:0}.alertify .ajs-footer{background:#fbfbfb;border-top:#eee 1px solid;border-radius:0 0 2px 2px}.alertify .ajs-footer .ajs-buttons .ajs-button{background-color:transparent;color:#000;border:0;font-size:14px;font-weight:700;text-transform:uppercase}.alertify .ajs-footer .ajs-buttons .ajs-button.ajs-ok{color:#3593D2}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:1px solid #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.css deleted file mode 100644 index 9fc93561..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.css +++ /dev/null @@ -1,84 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer { - background-color: rgba(0, 0, 0, 0.85); - opacity: 1; -} -.alertify .ajs-dialog { - max-width: 50%; - min-height: 137px; - background-color: #F4F4F4; - border: 1px solid #DDD; - box-shadow: none; - border-radius: 5px; -} -.alertify .ajs-header { - padding: 1.5rem 2rem; - border-bottom: none; - border-radius: 5px 5px 0 0; - color: #555; - background-color: #fff; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 1.6em; - font-weight: 700; -} -.alertify .ajs-body { - font-family: 'Roboto', sans-serif; - color: #555; -} -.alertify .ajs-body .ajs-content .ajs-input { - width: 100%; - margin: 0; - padding: .65em 1em; - font-size: 1em; - background-color: #FFF; - border: 1px solid rgba(0, 0, 0, 0.15); - outline: 0; - color: rgba(0, 0, 0, 0.7); - border-radius: .3125em; - transition: background-color 0.3s ease-out, box-shadow 0.2s ease, border-color 0.2s ease; - box-sizing: border-box; -} -.alertify .ajs-body .ajs-content .ajs-input:active { - border-color: rgba(0, 0, 0, 0.3); - background-color: #FAFAFA; -} -.alertify .ajs-body .ajs-content .ajs-input:focus { - border-color: rgba(0, 0, 0, 0.2); - color: rgba(0, 0, 0, 0.85); -} -.alertify.ajs-resizable .ajs-content, -.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content { - top: 64px; - bottom: 74px; -} -.alertify .ajs-footer { - background-color: #fff; - padding: 1rem 2rem; - border-top: none; - border-radius: 0 0 5px 5px; -} -.alertify-notifier .ajs-message { - background: rgba(255, 255, 255, 0.95); - color: #000; - text-align: center; - border: solid 1px #ddd; - border-radius: 2px; -} -.alertify-notifier .ajs-message.ajs-success { - color: #fff; - background: rgba(91, 189, 114, 0.95); - text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-error { - color: #fff; - background: rgba(217, 92, 92, 0.95); - text-shadow: -1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-warning { - background: rgba(252, 248, 215, 0.95); - border-color: #999; -} diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.min.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.min.css deleted file mode 100644 index 03fc6147..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer{background-color:rgba(0,0,0,.85);opacity:1}.alertify .ajs-dialog{max-width:50%;min-height:137px;background-color:#F4F4F4;border:1px solid #DDD;box-shadow:none;border-radius:5px}.alertify .ajs-header{padding:1.5rem 2rem;border-bottom:none;border-radius:5px 5px 0 0;color:#555;background-color:#fff;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:1.6em;font-weight:700}.alertify .ajs-body{font-family:Roboto,sans-serif;color:#555}.alertify .ajs-body .ajs-content .ajs-input{width:100%;margin:0;padding:.65em 1em;font-size:1em;background-color:#FFF;border:1px solid rgba(0,0,0,.15);outline:0;color:rgba(0,0,0,.7);border-radius:.3125em;transition:background-color .3s ease-out,box-shadow .2s ease,border-color .2s ease;box-sizing:border-box}.alertify .ajs-body .ajs-content .ajs-input:active{border-color:rgba(0,0,0,.3);background-color:#FAFAFA}.alertify .ajs-body .ajs-content .ajs-input:focus{border-color:rgba(0,0,0,.2);color:rgba(0,0,0,.85)}.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content,.alertify.ajs-resizable .ajs-content{top:64px;bottom:74px}.alertify .ajs-footer{background-color:#fff;padding:1rem 2rem;border-top:none;border-radius:0 0 5px 5px}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:1px solid #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:-1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.rtl.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.rtl.css deleted file mode 100644 index b36c246d..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.rtl.css +++ /dev/null @@ -1,84 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer { - background-color: rgba(0, 0, 0, 0.85); - opacity: 1; -} -.alertify .ajs-dialog { - max-width: 50%; - min-height: 137px; - background-color: #F4F4F4; - border: 1px solid #DDD; - box-shadow: none; - border-radius: 5px; -} -.alertify .ajs-header { - padding: 1.5rem 2rem; - border-bottom: none; - border-radius: 5px 5px 0 0; - color: #555; - background-color: #fff; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 1.6em; - font-weight: 700; -} -.alertify .ajs-body { - font-family: 'Roboto', sans-serif; - color: #555; -} -.alertify .ajs-body .ajs-content .ajs-input { - width: 100%; - margin: 0; - padding: .65em 1em; - font-size: 1em; - background-color: #FFF; - border: 1px solid rgba(0, 0, 0, 0.15); - outline: 0; - color: rgba(0, 0, 0, 0.7); - border-radius: .3125em; - transition: background-color 0.3s ease-out, box-shadow 0.2s ease, border-color 0.2s ease; - box-sizing: border-box; -} -.alertify .ajs-body .ajs-content .ajs-input:active { - border-color: rgba(0, 0, 0, 0.3); - background-color: #FAFAFA; -} -.alertify .ajs-body .ajs-content .ajs-input:focus { - border-color: rgba(0, 0, 0, 0.2); - color: rgba(0, 0, 0, 0.85); -} -.alertify.ajs-resizable .ajs-content, -.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content { - top: 64px; - bottom: 74px; -} -.alertify .ajs-footer { - background-color: #fff; - padding: 1rem 2rem; - border-top: none; - border-radius: 0 0 5px 5px; -} -.alertify-notifier .ajs-message { - background: rgba(255, 255, 255, 0.95); - color: #000; - text-align: center; - border: solid 1px #ddd; - border-radius: 2px; -} -.alertify-notifier .ajs-message.ajs-success { - color: #fff; - background: rgba(91, 189, 114, 0.95); - text-shadow: 1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-error { - color: #fff; - background: rgba(217, 92, 92, 0.95); - text-shadow: 1px -1px 0 rgba(0, 0, 0, 0.5); -} -.alertify-notifier .ajs-message.ajs-warning { - background: rgba(252, 248, 215, 0.95); - border-color: #999; -} diff --git a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.rtl.min.css b/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.rtl.min.css deleted file mode 100644 index 3ad62363..00000000 --- a/src/Server/OneTrueError.Web/Content/alertifyjs/themes/semantic.rtl.min.css +++ /dev/null @@ -1,6 +0,0 @@ -/** - * alertifyjs 1.7.1 http://alertifyjs.com - * AlertifyJS is a javascript framework for developing pretty browser dialogs and notifications. - * Copyright 2016 Mohammad Younes (http://alertifyjs.com) - * Licensed under MIT */ -.alertify .ajs-dimmer{background-color:rgba(0,0,0,.85);opacity:1}.alertify .ajs-dialog{max-width:50%;min-height:137px;background-color:#F4F4F4;border:1px solid #DDD;box-shadow:none;border-radius:5px}.alertify .ajs-header{padding:1.5rem 2rem;border-bottom:none;border-radius:5px 5px 0 0;color:#555;background-color:#fff;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:1.6em;font-weight:700}.alertify .ajs-body{font-family:Roboto,sans-serif;color:#555}.alertify .ajs-body .ajs-content .ajs-input{width:100%;margin:0;padding:.65em 1em;font-size:1em;background-color:#FFF;border:1px solid rgba(0,0,0,.15);outline:0;color:rgba(0,0,0,.7);border-radius:.3125em;transition:background-color .3s ease-out,box-shadow .2s ease,border-color .2s ease;box-sizing:border-box}.alertify .ajs-body .ajs-content .ajs-input:active{border-color:rgba(0,0,0,.3);background-color:#FAFAFA}.alertify .ajs-body .ajs-content .ajs-input:focus{border-color:rgba(0,0,0,.2);color:rgba(0,0,0,.85)}.alertify.ajs-maximized:not(.ajs-resizable) .ajs-content,.alertify.ajs-resizable .ajs-content{top:64px;bottom:74px}.alertify .ajs-footer{background-color:#fff;padding:1rem 2rem;border-top:none;border-radius:0 0 5px 5px}.alertify-notifier .ajs-message{background:rgba(255,255,255,.95);color:#000;text-align:center;border:1px solid #ddd;border-radius:2px}.alertify-notifier .ajs-message.ajs-success{color:#fff;background:rgba(91,189,114,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-error{color:#fff;background:rgba(217,92,92,.95);text-shadow:1px -1px 0 rgba(0,0,0,.5)}.alertify-notifier .ajs-message.ajs-warning{background:rgba(252,248,215,.95);border-color:#999} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/bootstrap-theme.css b/src/Server/OneTrueError.Web/Content/bootstrap-theme.css deleted file mode 100644 index c19cd5c4..00000000 --- a/src/Server/OneTrueError.Web/Content/bootstrap-theme.css +++ /dev/null @@ -1,587 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -.btn-default, -.btn-primary, -.btn-success, -.btn-info, -.btn-warning, -.btn-danger { - text-shadow: 0 -1px 0 rgba(0, 0, 0, .2); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 1px rgba(0, 0, 0, .075); -} -.btn-default:active, -.btn-primary:active, -.btn-success:active, -.btn-info:active, -.btn-warning:active, -.btn-danger:active, -.btn-default.active, -.btn-primary.active, -.btn-success.active, -.btn-info.active, -.btn-warning.active, -.btn-danger.active { - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); -} -.btn-default.disabled, -.btn-primary.disabled, -.btn-success.disabled, -.btn-info.disabled, -.btn-warning.disabled, -.btn-danger.disabled, -.btn-default[disabled], -.btn-primary[disabled], -.btn-success[disabled], -.btn-info[disabled], -.btn-warning[disabled], -.btn-danger[disabled], -fieldset[disabled] .btn-default, -fieldset[disabled] .btn-primary, -fieldset[disabled] .btn-success, -fieldset[disabled] .btn-info, -fieldset[disabled] .btn-warning, -fieldset[disabled] .btn-danger { - -webkit-box-shadow: none; - box-shadow: none; -} -.btn-default .badge, -.btn-primary .badge, -.btn-success .badge, -.btn-info .badge, -.btn-warning .badge, -.btn-danger .badge { - text-shadow: none; -} -.btn:active, -.btn.active { - background-image: none; -} -.btn-default { - text-shadow: 0 1px 0 #fff; - background-image: -webkit-linear-gradient(top, #fff 0%, #e0e0e0 100%); - background-image: -o-linear-gradient(top, #fff 0%, #e0e0e0 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#e0e0e0)); - background-image: linear-gradient(to bottom, #fff 0%, #e0e0e0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #dbdbdb; - border-color: #ccc; -} -.btn-default:hover, -.btn-default:focus { - background-color: #e0e0e0; - background-position: 0 -15px; -} -.btn-default:active, -.btn-default.active { - background-color: #e0e0e0; - border-color: #dbdbdb; -} -.btn-default.disabled, -.btn-default[disabled], -fieldset[disabled] .btn-default, -.btn-default.disabled:hover, -.btn-default[disabled]:hover, -fieldset[disabled] .btn-default:hover, -.btn-default.disabled:focus, -.btn-default[disabled]:focus, -fieldset[disabled] .btn-default:focus, -.btn-default.disabled.focus, -.btn-default[disabled].focus, -fieldset[disabled] .btn-default.focus, -.btn-default.disabled:active, -.btn-default[disabled]:active, -fieldset[disabled] .btn-default:active, -.btn-default.disabled.active, -.btn-default[disabled].active, -fieldset[disabled] .btn-default.active { - background-color: #e0e0e0; - background-image: none; -} -.btn-primary { - background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%); - background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#265a88)); - background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #245580; -} -.btn-primary:hover, -.btn-primary:focus { - background-color: #265a88; - background-position: 0 -15px; -} -.btn-primary:active, -.btn-primary.active { - background-color: #265a88; - border-color: #245580; -} -.btn-primary.disabled, -.btn-primary[disabled], -fieldset[disabled] .btn-primary, -.btn-primary.disabled:hover, -.btn-primary[disabled]:hover, -fieldset[disabled] .btn-primary:hover, -.btn-primary.disabled:focus, -.btn-primary[disabled]:focus, -fieldset[disabled] .btn-primary:focus, -.btn-primary.disabled.focus, -.btn-primary[disabled].focus, -fieldset[disabled] .btn-primary.focus, -.btn-primary.disabled:active, -.btn-primary[disabled]:active, -fieldset[disabled] .btn-primary:active, -.btn-primary.disabled.active, -.btn-primary[disabled].active, -fieldset[disabled] .btn-primary.active { - background-color: #265a88; - background-image: none; -} -.btn-success { - background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%); - background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#419641)); - background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #3e8f3e; -} -.btn-success:hover, -.btn-success:focus { - background-color: #419641; - background-position: 0 -15px; -} -.btn-success:active, -.btn-success.active { - background-color: #419641; - border-color: #3e8f3e; -} -.btn-success.disabled, -.btn-success[disabled], -fieldset[disabled] .btn-success, -.btn-success.disabled:hover, -.btn-success[disabled]:hover, -fieldset[disabled] .btn-success:hover, -.btn-success.disabled:focus, -.btn-success[disabled]:focus, -fieldset[disabled] .btn-success:focus, -.btn-success.disabled.focus, -.btn-success[disabled].focus, -fieldset[disabled] .btn-success.focus, -.btn-success.disabled:active, -.btn-success[disabled]:active, -fieldset[disabled] .btn-success:active, -.btn-success.disabled.active, -.btn-success[disabled].active, -fieldset[disabled] .btn-success.active { - background-color: #419641; - background-image: none; -} -.btn-info { - background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); - background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#2aabd2)); - background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #28a4c9; -} -.btn-info:hover, -.btn-info:focus { - background-color: #2aabd2; - background-position: 0 -15px; -} -.btn-info:active, -.btn-info.active { - background-color: #2aabd2; - border-color: #28a4c9; -} -.btn-info.disabled, -.btn-info[disabled], -fieldset[disabled] .btn-info, -.btn-info.disabled:hover, -.btn-info[disabled]:hover, -fieldset[disabled] .btn-info:hover, -.btn-info.disabled:focus, -.btn-info[disabled]:focus, -fieldset[disabled] .btn-info:focus, -.btn-info.disabled.focus, -.btn-info[disabled].focus, -fieldset[disabled] .btn-info.focus, -.btn-info.disabled:active, -.btn-info[disabled]:active, -fieldset[disabled] .btn-info:active, -.btn-info.disabled.active, -.btn-info[disabled].active, -fieldset[disabled] .btn-info.active { - background-color: #2aabd2; - background-image: none; -} -.btn-warning { - background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); - background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#eb9316)); - background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #e38d13; -} -.btn-warning:hover, -.btn-warning:focus { - background-color: #eb9316; - background-position: 0 -15px; -} -.btn-warning:active, -.btn-warning.active { - background-color: #eb9316; - border-color: #e38d13; -} -.btn-warning.disabled, -.btn-warning[disabled], -fieldset[disabled] .btn-warning, -.btn-warning.disabled:hover, -.btn-warning[disabled]:hover, -fieldset[disabled] .btn-warning:hover, -.btn-warning.disabled:focus, -.btn-warning[disabled]:focus, -fieldset[disabled] .btn-warning:focus, -.btn-warning.disabled.focus, -.btn-warning[disabled].focus, -fieldset[disabled] .btn-warning.focus, -.btn-warning.disabled:active, -.btn-warning[disabled]:active, -fieldset[disabled] .btn-warning:active, -.btn-warning.disabled.active, -.btn-warning[disabled].active, -fieldset[disabled] .btn-warning.active { - background-color: #eb9316; - background-image: none; -} -.btn-danger { - background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%); - background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c12e2a)); - background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-color: #b92c28; -} -.btn-danger:hover, -.btn-danger:focus { - background-color: #c12e2a; - background-position: 0 -15px; -} -.btn-danger:active, -.btn-danger.active { - background-color: #c12e2a; - border-color: #b92c28; -} -.btn-danger.disabled, -.btn-danger[disabled], -fieldset[disabled] .btn-danger, -.btn-danger.disabled:hover, -.btn-danger[disabled]:hover, -fieldset[disabled] .btn-danger:hover, -.btn-danger.disabled:focus, -.btn-danger[disabled]:focus, -fieldset[disabled] .btn-danger:focus, -.btn-danger.disabled.focus, -.btn-danger[disabled].focus, -fieldset[disabled] .btn-danger.focus, -.btn-danger.disabled:active, -.btn-danger[disabled]:active, -fieldset[disabled] .btn-danger:active, -.btn-danger.disabled.active, -.btn-danger[disabled].active, -fieldset[disabled] .btn-danger.active { - background-color: #c12e2a; - background-image: none; -} -.thumbnail, -.img-thumbnail { - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); - box-shadow: 0 1px 2px rgba(0, 0, 0, .075); -} -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - background-color: #e8e8e8; - background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); - background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); - background-repeat: repeat-x; -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - background-color: #2e6da4; - background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); - background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); - background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); - background-repeat: repeat-x; -} -.navbar-default { - background-image: -webkit-linear-gradient(top, #fff 0%, #f8f8f8 100%); - background-image: -o-linear-gradient(top, #fff 0%, #f8f8f8 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#f8f8f8)); - background-image: linear-gradient(to bottom, #fff 0%, #f8f8f8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .15), 0 1px 5px rgba(0, 0, 0, .075); -} -.navbar-default .navbar-nav > .open > a, -.navbar-default .navbar-nav > .active > a { - background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); - background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2)); - background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0); - background-repeat: repeat-x; - -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); - box-shadow: inset 0 3px 9px rgba(0, 0, 0, .075); -} -.navbar-brand, -.navbar-nav > li > a { - text-shadow: 0 1px 0 rgba(255, 255, 255, .25); -} -.navbar-inverse { - background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222 100%); - background-image: -o-linear-gradient(top, #3c3c3c 0%, #222 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222)); - background-image: linear-gradient(to bottom, #3c3c3c 0%, #222 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0); - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); - background-repeat: repeat-x; - border-radius: 4px; -} -.navbar-inverse .navbar-nav > .open > a, -.navbar-inverse .navbar-nav > .active > a { - background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%); - background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f)); - background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0); - background-repeat: repeat-x; - -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); - box-shadow: inset 0 3px 9px rgba(0, 0, 0, .25); -} -.navbar-inverse .navbar-brand, -.navbar-inverse .navbar-nav > li > a { - text-shadow: 0 -1px 0 rgba(0, 0, 0, .25); -} -.navbar-static-top, -.navbar-fixed-top, -.navbar-fixed-bottom { - border-radius: 0; -} -@media (max-width: 767px) { - .navbar .navbar-nav .open .dropdown-menu > .active > a, - .navbar .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #fff; - background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); - background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); - background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); - background-repeat: repeat-x; - } -} -.alert { - text-shadow: 0 1px 0 rgba(255, 255, 255, .2); - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25), 0 1px 2px rgba(0, 0, 0, .05); -} -.alert-success { - background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); - background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#c8e5bc)); - background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0); - background-repeat: repeat-x; - border-color: #b2dba1; -} -.alert-info { - background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%); - background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#b9def0)); - background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0); - background-repeat: repeat-x; - border-color: #9acfea; -} -.alert-warning { - background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); - background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#f8efc0)); - background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0); - background-repeat: repeat-x; - border-color: #f5e79e; -} -.alert-danger { - background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); - background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#e7c3c3)); - background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0); - background-repeat: repeat-x; - border-color: #dca7a7; -} -.progress { - background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); - background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5)); - background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar { - background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%); - background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#286090)); - background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-success { - background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%); - background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#5cb85c), to(#449d44)); - background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-info { - background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); - background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#5bc0de), to(#31b0d5)); - background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-warning { - background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); - background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f0ad4e), to(#ec971f)); - background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-danger { - background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%); - background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#d9534f), to(#c9302c)); - background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0); - background-repeat: repeat-x; -} -.progress-bar-striped { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.list-group { - border-radius: 4px; - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .075); - box-shadow: 0 1px 2px rgba(0, 0, 0, .075); -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - text-shadow: 0 -1px 0 #286090; - background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%); - background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2b669a)); - background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0); - background-repeat: repeat-x; - border-color: #2b669a; -} -.list-group-item.active .badge, -.list-group-item.active:hover .badge, -.list-group-item.active:focus .badge { - text-shadow: none; -} -.panel { - -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, .05); - box-shadow: 0 1px 2px rgba(0, 0, 0, .05); -} -.panel-default > .panel-heading { - background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8)); - background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0); - background-repeat: repeat-x; -} -.panel-primary > .panel-heading { - background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%); - background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#337ab7), to(#2e6da4)); - background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0); - background-repeat: repeat-x; -} -.panel-success > .panel-heading { - background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); - background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#dff0d8), to(#d0e9c6)); - background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0); - background-repeat: repeat-x; -} -.panel-info > .panel-heading { - background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); - background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#d9edf7), to(#c4e3f3)); - background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0); - background-repeat: repeat-x; -} -.panel-warning > .panel-heading { - background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); - background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#fcf8e3), to(#faf2cc)); - background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0); - background-repeat: repeat-x; -} -.panel-danger > .panel-heading { - background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%); - background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#f2dede), to(#ebcccc)); - background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0); - background-repeat: repeat-x; -} -.well { - background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); - background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%); - background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5)); - background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0); - background-repeat: repeat-x; - border-color: #dcdcdc; - -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .05), 0 1px 0 rgba(255, 255, 255, .1); -} -/*# sourceMappingURL=bootstrap-theme.css.map */ diff --git a/src/Server/OneTrueError.Web/Content/bootstrap-theme.css.map b/src/Server/OneTrueError.Web/Content/bootstrap-theme.css.map deleted file mode 100644 index 75353114..00000000 --- a/src/Server/OneTrueError.Web/Content/bootstrap-theme.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["bootstrap-theme.css","less/theme.less","less/mixins/vendor-prefixes.less","less/mixins/gradients.less","less/mixins/reset-filter.less"],"names":[],"mappings":"AAAA;;;;GAIG;ACeH;;;;;;EAME,yCAAA;EC2CA,4FAAA;EACQ,oFAAA;CFvDT;ACgBC;;;;;;;;;;;;ECsCA,yDAAA;EACQ,iDAAA;CFxCT;ACMC;;;;;;;;;;;;;;;;;;ECiCA,yBAAA;EACQ,iBAAA;CFnBT;AC/BD;;;;;;EAuBI,kBAAA;CDgBH;ACyBC;;EAEE,uBAAA;CDvBH;AC4BD;EErEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;EAuC2C,0BAAA;EAA2B,mBAAA;CDjBvE;ACpBC;;EAEE,0BAAA;EACA,6BAAA;CDsBH;ACnBC;;EAEE,0BAAA;EACA,sBAAA;CDqBH;ACfG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD6BL;ACbD;EEtEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CD8DD;AC5DC;;EAEE,0BAAA;EACA,6BAAA;CD8DH;AC3DC;;EAEE,0BAAA;EACA,sBAAA;CD6DH;ACvDG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDqEL;ACpDD;EEvEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CDsGD;ACpGC;;EAEE,0BAAA;EACA,6BAAA;CDsGH;ACnGC;;EAEE,0BAAA;EACA,sBAAA;CDqGH;AC/FG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD6GL;AC3FD;EExEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CD8ID;AC5IC;;EAEE,0BAAA;EACA,6BAAA;CD8IH;AC3IC;;EAEE,0BAAA;EACA,sBAAA;CD6IH;ACvIG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDqJL;AClID;EEzEI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CDsLD;ACpLC;;EAEE,0BAAA;EACA,6BAAA;CDsLH;ACnLC;;EAEE,0BAAA;EACA,sBAAA;CDqLH;AC/KG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CD6LL;ACzKD;EE1EI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EAEA,uHAAA;ECnBF,oEAAA;EH4CA,4BAAA;EACA,sBAAA;CD8ND;AC5NC;;EAEE,0BAAA;EACA,6BAAA;CD8NH;AC3NC;;EAEE,0BAAA;EACA,sBAAA;CD6NH;ACvNG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACA,uBAAA;CDqOL;AC1MD;;EClCE,mDAAA;EACQ,2CAAA;CFgPT;ACrMD;;EE3FI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF0FF,0BAAA;CD2MD;ACzMD;;;EEhGI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EFgGF,0BAAA;CD+MD;ACtMD;EE7GI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;ECnBF,oEAAA;EH+HA,mBAAA;ECjEA,4FAAA;EACQ,oFAAA;CF8QT;ACjND;;EE7GI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;ED2CF,yDAAA;EACQ,iDAAA;CFwRT;AC9MD;;EAEE,+CAAA;CDgND;AC5MD;EEhII,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;ECnBF,oEAAA;EHkJA,mBAAA;CDkND;ACrND;;EEhII,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;ED2CF,wDAAA;EACQ,gDAAA;CF+ST;AC/ND;;EAYI,0CAAA;CDuNH;AClND;;;EAGE,iBAAA;CDoND;AC/LD;EAfI;;;IAGE,YAAA;IE7JF,yEAAA;IACA,oEAAA;IACA,8FAAA;IAAA,uEAAA;IACA,4BAAA;IACA,uHAAA;GH+WD;CACF;AC3MD;EACE,8CAAA;EC3HA,2FAAA;EACQ,mFAAA;CFyUT;ACnMD;EEtLI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF8KF,sBAAA;CD+MD;AC1MD;EEvLI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF8KF,sBAAA;CDuND;ACjND;EExLI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF8KF,sBAAA;CD+ND;ACxND;EEzLI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EF8KF,sBAAA;CDuOD;ACxND;EEjMI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CH4ZH;ACrND;EE3MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHmaH;AC3ND;EE5MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CH0aH;ACjOD;EE7MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHibH;ACvOD;EE9MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHwbH;AC7OD;EE/MI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CH+bH;AChPD;EElLI,8MAAA;EACA,yMAAA;EACA,sMAAA;CHqaH;AC5OD;EACE,mBAAA;EC9KA,mDAAA;EACQ,2CAAA;CF6ZT;AC7OD;;;EAGE,8BAAA;EEnOE,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EFiOF,sBAAA;CDmPD;ACxPD;;;EAQI,kBAAA;CDqPH;AC3OD;ECnME,kDAAA;EACQ,0CAAA;CFibT;ACrOD;EE5PI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHoeH;AC3OD;EE7PI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CH2eH;ACjPD;EE9PI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHkfH;ACvPD;EE/PI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHyfH;AC7PD;EEhQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHggBH;ACnQD;EEjQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;CHugBH;ACnQD;EExQI,yEAAA;EACA,oEAAA;EACA,8FAAA;EAAA,uEAAA;EACA,4BAAA;EACA,uHAAA;EFsQF,sBAAA;EC3NA,0FAAA;EACQ,kFAAA;CFqeT","file":"bootstrap-theme.css","sourcesContent":["/*!\n * Bootstrap v3.3.5 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.btn-default:active,\n.btn-primary:active,\n.btn-success:active,\n.btn-info:active,\n.btn-warning:active,\n.btn-danger:active,\n.btn-default.active,\n.btn-primary.active,\n.btn-success.active,\n.btn-info.active,\n.btn-warning.active,\n.btn-danger.active {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-default.disabled,\n.btn-primary.disabled,\n.btn-success.disabled,\n.btn-info.disabled,\n.btn-warning.disabled,\n.btn-danger.disabled,\n.btn-default[disabled],\n.btn-primary[disabled],\n.btn-success[disabled],\n.btn-info[disabled],\n.btn-warning[disabled],\n.btn-danger[disabled],\nfieldset[disabled] .btn-default,\nfieldset[disabled] .btn-primary,\nfieldset[disabled] .btn-success,\nfieldset[disabled] .btn-info,\nfieldset[disabled] .btn-warning,\nfieldset[disabled] .btn-danger {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default .badge,\n.btn-primary .badge,\n.btn-success .badge,\n.btn-info .badge,\n.btn-warning .badge,\n.btn-danger .badge {\n text-shadow: none;\n}\n.btn:active,\n.btn.active {\n background-image: none;\n}\n.btn-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #dbdbdb;\n text-shadow: 0 1px 0 #fff;\n border-color: #ccc;\n}\n.btn-default:hover,\n.btn-default:focus {\n background-color: #e0e0e0;\n background-position: 0 -15px;\n}\n.btn-default:active,\n.btn-default.active {\n background-color: #e0e0e0;\n border-color: #dbdbdb;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #e0e0e0;\n background-image: none;\n}\n.btn-primary {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #265a88 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #265a88 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #245580;\n}\n.btn-primary:hover,\n.btn-primary:focus {\n background-color: #265a88;\n background-position: 0 -15px;\n}\n.btn-primary:active,\n.btn-primary.active {\n background-color: #265a88;\n border-color: #245580;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #265a88;\n background-image: none;\n}\n.btn-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #419641 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #419641 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #3e8f3e;\n}\n.btn-success:hover,\n.btn-success:focus {\n background-color: #419641;\n background-position: 0 -15px;\n}\n.btn-success:active,\n.btn-success.active {\n background-color: #419641;\n border-color: #3e8f3e;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #419641;\n background-image: none;\n}\n.btn-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #2aabd2 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #2aabd2 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #28a4c9;\n}\n.btn-info:hover,\n.btn-info:focus {\n background-color: #2aabd2;\n background-position: 0 -15px;\n}\n.btn-info:active,\n.btn-info.active {\n background-color: #2aabd2;\n border-color: #28a4c9;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #2aabd2;\n background-image: none;\n}\n.btn-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #eb9316 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #eb9316 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #e38d13;\n}\n.btn-warning:hover,\n.btn-warning:focus {\n background-color: #eb9316;\n background-position: 0 -15px;\n}\n.btn-warning:active,\n.btn-warning.active {\n background-color: #eb9316;\n border-color: #e38d13;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #eb9316;\n background-image: none;\n}\n.btn-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c12e2a 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c12e2a 100%);\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n background-repeat: repeat-x;\n border-color: #b92c28;\n}\n.btn-danger:hover,\n.btn-danger:focus {\n background-color: #c12e2a;\n background-position: 0 -15px;\n}\n.btn-danger:active,\n.btn-danger.active {\n background-color: #c12e2a;\n border-color: #b92c28;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #c12e2a;\n background-image: none;\n}\n.thumbnail,\n.img-thumbnail {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n background-color: #e8e8e8;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n background-color: #2e6da4;\n}\n.navbar-default {\n background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);\n background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);\n background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);\n}\n.navbar-inverse {\n background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%);\n background-image: -o-linear-gradient(top, #3c3c3c 0%, #222222 100%);\n background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n border-radius: 4px;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .active > a {\n background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);\n background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);\n -webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);\n}\n.navbar-inverse .navbar-brand,\n.navbar-inverse .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n@media (max-width: 767px) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #fff;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n }\n}\n.alert {\n text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.alert-success {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #c8e5bc 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #c8e5bc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);\n border-color: #b2dba1;\n}\n.alert-info {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #b9def0 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #b9def0 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);\n border-color: #9acfea;\n}\n.alert-warning {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #f8efc0 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #f8efc0 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);\n border-color: #f5e79e;\n}\n.alert-danger {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #e7c3c3 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #e7c3c3 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);\n border-color: #dca7a7;\n}\n.progress {\n background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);\n}\n.progress-bar {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #286090 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #286090 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);\n}\n.progress-bar-success {\n background-image: -webkit-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: -o-linear-gradient(top, #5cb85c 0%, #449d44 100%);\n background-image: linear-gradient(to bottom, #5cb85c 0%, #449d44 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);\n}\n.progress-bar-info {\n background-image: -webkit-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: -o-linear-gradient(top, #5bc0de 0%, #31b0d5 100%);\n background-image: linear-gradient(to bottom, #5bc0de 0%, #31b0d5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);\n}\n.progress-bar-warning {\n background-image: -webkit-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: -o-linear-gradient(top, #f0ad4e 0%, #ec971f 100%);\n background-image: linear-gradient(to bottom, #f0ad4e 0%, #ec971f 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);\n}\n.progress-bar-danger {\n background-image: -webkit-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: -o-linear-gradient(top, #d9534f 0%, #c9302c 100%);\n background-image: linear-gradient(to bottom, #d9534f 0%, #c9302c 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);\n}\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.list-group {\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 #286090;\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2b669a 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2b669a 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);\n border-color: #2b669a;\n}\n.list-group-item.active .badge,\n.list-group-item.active:hover .badge,\n.list-group-item.active:focus .badge {\n text-shadow: none;\n}\n.panel {\n -webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);\n}\n.panel-default > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);\n background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);\n}\n.panel-primary > .panel-heading {\n background-image: -webkit-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: -o-linear-gradient(top, #337ab7 0%, #2e6da4 100%);\n background-image: linear-gradient(to bottom, #337ab7 0%, #2e6da4 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);\n}\n.panel-success > .panel-heading {\n background-image: -webkit-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: -o-linear-gradient(top, #dff0d8 0%, #d0e9c6 100%);\n background-image: linear-gradient(to bottom, #dff0d8 0%, #d0e9c6 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);\n}\n.panel-info > .panel-heading {\n background-image: -webkit-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: -o-linear-gradient(top, #d9edf7 0%, #c4e3f3 100%);\n background-image: linear-gradient(to bottom, #d9edf7 0%, #c4e3f3 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);\n}\n.panel-warning > .panel-heading {\n background-image: -webkit-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: -o-linear-gradient(top, #fcf8e3 0%, #faf2cc 100%);\n background-image: linear-gradient(to bottom, #fcf8e3 0%, #faf2cc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);\n}\n.panel-danger > .panel-heading {\n background-image: -webkit-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: -o-linear-gradient(top, #f2dede 0%, #ebcccc 100%);\n background-image: linear-gradient(to bottom, #f2dede 0%, #ebcccc 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);\n}\n.well {\n background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);\n background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);\n border-color: #dcdcdc;\n -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);\n}\n/*# sourceMappingURL=bootstrap-theme.css.map */","/*!\n * Bootstrap v3.3.5 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n\n//\n// Load core variables and mixins\n// --------------------------------------------------\n\n@import \"variables.less\";\n@import \"mixins.less\";\n\n\n//\n// Buttons\n// --------------------------------------------------\n\n// Common styles\n.btn-default,\n.btn-primary,\n.btn-success,\n.btn-info,\n.btn-warning,\n.btn-danger {\n text-shadow: 0 -1px 0 rgba(0,0,0,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 1px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n // Reset the shadow\n &:active,\n &.active {\n .box-shadow(inset 0 3px 5px rgba(0,0,0,.125));\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n .box-shadow(none);\n }\n\n .badge {\n text-shadow: none;\n }\n}\n\n// Mixin for generating new styles\n.btn-styles(@btn-color: #555) {\n #gradient > .vertical(@start-color: @btn-color; @end-color: darken(@btn-color, 12%));\n .reset-filter(); // Disable gradients for IE9 because filter bleeds through rounded corners; see https://github.com/twbs/bootstrap/issues/10620\n background-repeat: repeat-x;\n border-color: darken(@btn-color, 14%);\n\n &:hover,\n &:focus {\n background-color: darken(@btn-color, 12%);\n background-position: 0 -15px;\n }\n\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n border-color: darken(@btn-color, 14%);\n }\n\n &.disabled,\n &[disabled],\n fieldset[disabled] & {\n &,\n &:hover,\n &:focus,\n &.focus,\n &:active,\n &.active {\n background-color: darken(@btn-color, 12%);\n background-image: none;\n }\n }\n}\n\n// Common styles\n.btn {\n // Remove the gradient for the pressed/active state\n &:active,\n &.active {\n background-image: none;\n }\n}\n\n// Apply the mixin to the buttons\n.btn-default { .btn-styles(@btn-default-bg); text-shadow: 0 1px 0 #fff; border-color: #ccc; }\n.btn-primary { .btn-styles(@btn-primary-bg); }\n.btn-success { .btn-styles(@btn-success-bg); }\n.btn-info { .btn-styles(@btn-info-bg); }\n.btn-warning { .btn-styles(@btn-warning-bg); }\n.btn-danger { .btn-styles(@btn-danger-bg); }\n\n\n//\n// Images\n// --------------------------------------------------\n\n.thumbnail,\n.img-thumbnail {\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n\n\n//\n// Dropdowns\n// --------------------------------------------------\n\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-hover-bg; @end-color: darken(@dropdown-link-hover-bg, 5%));\n background-color: darken(@dropdown-link-hover-bg, 5%);\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n background-color: darken(@dropdown-link-active-bg, 5%);\n}\n\n\n//\n// Navbar\n// --------------------------------------------------\n\n// Default navbar\n.navbar-default {\n #gradient > .vertical(@start-color: lighten(@navbar-default-bg, 10%); @end-color: @navbar-default-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered\n border-radius: @navbar-border-radius;\n @shadow: inset 0 1px 0 rgba(255,255,255,.15), 0 1px 5px rgba(0,0,0,.075);\n .box-shadow(@shadow);\n\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: darken(@navbar-default-link-active-bg, 5%); @end-color: darken(@navbar-default-link-active-bg, 2%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.075));\n }\n}\n.navbar-brand,\n.navbar-nav > li > a {\n text-shadow: 0 1px 0 rgba(255,255,255,.25);\n}\n\n// Inverted navbar\n.navbar-inverse {\n #gradient > .vertical(@start-color: lighten(@navbar-inverse-bg, 10%); @end-color: @navbar-inverse-bg);\n .reset-filter(); // Remove gradient in IE<10 to fix bug where dropdowns don't get triggered; see https://github.com/twbs/bootstrap/issues/10257\n border-radius: @navbar-border-radius;\n .navbar-nav > .open > a,\n .navbar-nav > .active > a {\n #gradient > .vertical(@start-color: @navbar-inverse-link-active-bg; @end-color: lighten(@navbar-inverse-link-active-bg, 2.5%));\n .box-shadow(inset 0 3px 9px rgba(0,0,0,.25));\n }\n\n .navbar-brand,\n .navbar-nav > li > a {\n text-shadow: 0 -1px 0 rgba(0,0,0,.25);\n }\n}\n\n// Undo rounded corners in static and fixed navbars\n.navbar-static-top,\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n border-radius: 0;\n}\n\n// Fix active state of dropdown items in collapsed mode\n@media (max-width: @grid-float-breakpoint-max) {\n .navbar .navbar-nav .open .dropdown-menu > .active > a {\n &,\n &:hover,\n &:focus {\n color: #fff;\n #gradient > .vertical(@start-color: @dropdown-link-active-bg; @end-color: darken(@dropdown-link-active-bg, 5%));\n }\n }\n}\n\n\n//\n// Alerts\n// --------------------------------------------------\n\n// Common styles\n.alert {\n text-shadow: 0 1px 0 rgba(255,255,255,.2);\n @shadow: inset 0 1px 0 rgba(255,255,255,.25), 0 1px 2px rgba(0,0,0,.05);\n .box-shadow(@shadow);\n}\n\n// Mixin for generating new styles\n.alert-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 7.5%));\n border-color: darken(@color, 15%);\n}\n\n// Apply the mixin to the alerts\n.alert-success { .alert-styles(@alert-success-bg); }\n.alert-info { .alert-styles(@alert-info-bg); }\n.alert-warning { .alert-styles(@alert-warning-bg); }\n.alert-danger { .alert-styles(@alert-danger-bg); }\n\n\n//\n// Progress bars\n// --------------------------------------------------\n\n// Give the progress background some depth\n.progress {\n #gradient > .vertical(@start-color: darken(@progress-bg, 4%); @end-color: @progress-bg)\n}\n\n// Mixin for generating new styles\n.progress-bar-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 10%));\n}\n\n// Apply the mixin to the progress bars\n.progress-bar { .progress-bar-styles(@progress-bar-bg); }\n.progress-bar-success { .progress-bar-styles(@progress-bar-success-bg); }\n.progress-bar-info { .progress-bar-styles(@progress-bar-info-bg); }\n.progress-bar-warning { .progress-bar-styles(@progress-bar-warning-bg); }\n.progress-bar-danger { .progress-bar-styles(@progress-bar-danger-bg); }\n\n// Reset the striped class because our mixins don't do multiple gradients and\n// the above custom styles override the new `.progress-bar-striped` in v3.2.0.\n.progress-bar-striped {\n #gradient > .striped();\n}\n\n\n//\n// List groups\n// --------------------------------------------------\n\n.list-group {\n border-radius: @border-radius-base;\n .box-shadow(0 1px 2px rgba(0,0,0,.075));\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n text-shadow: 0 -1px 0 darken(@list-group-active-bg, 10%);\n #gradient > .vertical(@start-color: @list-group-active-bg; @end-color: darken(@list-group-active-bg, 7.5%));\n border-color: darken(@list-group-active-border, 7.5%);\n\n .badge {\n text-shadow: none;\n }\n}\n\n\n//\n// Panels\n// --------------------------------------------------\n\n// Common styles\n.panel {\n .box-shadow(0 1px 2px rgba(0,0,0,.05));\n}\n\n// Mixin for generating new styles\n.panel-heading-styles(@color) {\n #gradient > .vertical(@start-color: @color; @end-color: darken(@color, 5%));\n}\n\n// Apply the mixin to the panel headings only\n.panel-default > .panel-heading { .panel-heading-styles(@panel-default-heading-bg); }\n.panel-primary > .panel-heading { .panel-heading-styles(@panel-primary-heading-bg); }\n.panel-success > .panel-heading { .panel-heading-styles(@panel-success-heading-bg); }\n.panel-info > .panel-heading { .panel-heading-styles(@panel-info-heading-bg); }\n.panel-warning > .panel-heading { .panel-heading-styles(@panel-warning-heading-bg); }\n.panel-danger > .panel-heading { .panel-heading-styles(@panel-danger-heading-bg); }\n\n\n//\n// Wells\n// --------------------------------------------------\n\n.well {\n #gradient > .vertical(@start-color: darken(@well-bg, 5%); @end-color: @well-bg);\n border-color: darken(@well-bg, 10%);\n @shadow: inset 0 1px 3px rgba(0,0,0,.05), 0 1px 0 rgba(255,255,255,.1);\n .box-shadow(@shadow);\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// Gradients\n\n#gradient {\n\n // Horizontal gradient, from left to right\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .horizontal(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(left, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to right, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n // Vertical gradient, from top to bottom\n //\n // Creates two color stops, start and end, by specifying a color and position for each color stop.\n // Color stops are not available in IE9 and below.\n .vertical(@start-color: #555; @end-color: #333; @start-percent: 0%; @end-percent: 100%) {\n background-image: -webkit-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(top, @start-color @start-percent, @end-color @end-percent); // Opera 12\n background-image: linear-gradient(to bottom, @start-color @start-percent, @end-color @end-percent); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n background-repeat: repeat-x;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down\n }\n\n .directional(@start-color: #555; @end-color: #333; @deg: 45deg) {\n background-repeat: repeat-x;\n background-image: -webkit-linear-gradient(@deg, @start-color, @end-color); // Safari 5.1-6, Chrome 10+\n background-image: -o-linear-gradient(@deg, @start-color, @end-color); // Opera 12\n background-image: linear-gradient(@deg, @start-color, @end-color); // Standard, IE10, Firefox 16+, Opera 12.10+, Safari 7+, Chrome 26+\n }\n .horizontal-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(left, @start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(to right, @start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=1)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .vertical-three-colors(@start-color: #00b3ee; @mid-color: #7a43b6; @color-stop: 50%; @end-color: #c3325f) {\n background-image: -webkit-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: -o-linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-image: linear-gradient(@start-color, @mid-color @color-stop, @end-color);\n background-repeat: no-repeat;\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(startColorstr='%d', endColorstr='%d', GradientType=0)\",argb(@start-color),argb(@end-color))); // IE9 and down, gets no color-stop at all for proper fallback\n }\n .radial(@inner-color: #555; @outer-color: #333) {\n background-image: -webkit-radial-gradient(circle, @inner-color, @outer-color);\n background-image: radial-gradient(circle, @inner-color, @outer-color);\n background-repeat: no-repeat;\n }\n .striped(@color: rgba(255,255,255,.15); @angle: 45deg) {\n background-image: -webkit-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n background-image: linear-gradient(@angle, @color 25%, transparent 25%, transparent 50%, @color 50%, @color 75%, transparent 75%, transparent);\n }\n}\n","// Reset filters for IE\n//\n// When you need to remove a gradient background, do not forget to use this to reset\n// the IE filter for IE9 and below.\n\n.reset-filter() {\n filter: e(%(\"progid:DXImageTransform.Microsoft.gradient(enabled = false)\"));\n}\n"]} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/bootstrap-theme.min.css b/src/Server/OneTrueError.Web/Content/bootstrap-theme.min.css deleted file mode 100644 index 61358b13..00000000 --- a/src/Server/OneTrueError.Web/Content/bootstrap-theme.min.css +++ /dev/null @@ -1,5 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-o-linear-gradient(top,#337ab7 0,#265a88 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#265a88));background-image:linear-gradient(to bottom,#337ab7 0,#265a88 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff265a88', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#245580}.btn-primary:focus,.btn-primary:hover{background-color:#265a88;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#265a88;border-color:#245580}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#265a88;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#419641 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#419641));background-image:linear-gradient(to bottom,#5cb85c 0,#419641 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff419641', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#3e8f3e}.btn-success:focus,.btn-success:hover{background-color:#419641;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#419641;border-color:#3e8f3e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#419641;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#2aabd2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#2aabd2));background-image:linear-gradient(to bottom,#5bc0de 0,#2aabd2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff2aabd2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#28a4c9}.btn-info:focus,.btn-info:hover{background-color:#2aabd2;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#2aabd2;border-color:#28a4c9}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#2aabd2;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#eb9316 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#eb9316));background-image:linear-gradient(to bottom,#f0ad4e 0,#eb9316 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffeb9316', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#e38d13}.btn-warning:focus,.btn-warning:hover{background-color:#eb9316;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#eb9316;border-color:#e38d13}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#eb9316;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c12e2a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c12e2a));background-image:linear-gradient(to bottom,#d9534f 0,#c12e2a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc12e2a', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b92c28}.btn-danger:focus,.btn-danger:hover{background-color:#c12e2a;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#c12e2a;border-color:#b92c28}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#c12e2a;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#2e6da4;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-o-linear-gradient(top,#3c3c3c 0,#222 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#3c3c3c),to(#222));background-image:linear-gradient(to bottom,#3c3c3c 0,#222 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-o-linear-gradient(top,#080808 0,#0f0f0f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#080808),to(#0f0f0f));background-image:linear-gradient(to bottom,#080808 0,#0f0f0f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#c8e5bc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#c8e5bc));background-image:linear-gradient(to bottom,#dff0d8 0,#c8e5bc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffc8e5bc', GradientType=0);background-repeat:repeat-x;border-color:#b2dba1}.alert-info{background-image:-webkit-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#b9def0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#b9def0));background-image:linear-gradient(to bottom,#d9edf7 0,#b9def0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffb9def0', GradientType=0);background-repeat:repeat-x;border-color:#9acfea}.alert-warning{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#f8efc0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#f8efc0));background-image:linear-gradient(to bottom,#fcf8e3 0,#f8efc0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fff8efc0', GradientType=0);background-repeat:repeat-x;border-color:#f5e79e}.alert-danger{background-image:-webkit-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-o-linear-gradient(top,#f2dede 0,#e7c3c3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#e7c3c3));background-image:linear-gradient(to bottom,#f2dede 0,#e7c3c3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffe7c3c3', GradientType=0);background-repeat:repeat-x;border-color:#dca7a7}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-o-linear-gradient(top,#337ab7 0,#286090 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#286090));background-image:linear-gradient(to bottom,#337ab7 0,#286090 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff286090', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-o-linear-gradient(top,#5cb85c 0,#449d44 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5cb85c),to(#449d44));background-image:linear-gradient(to bottom,#5cb85c 0,#449d44 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5cb85c', endColorstr='#ff449d44', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-o-linear-gradient(top,#5bc0de 0,#31b0d5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#5bc0de),to(#31b0d5));background-image:linear-gradient(to bottom,#5bc0de 0,#31b0d5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff5bc0de', endColorstr='#ff31b0d5', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-o-linear-gradient(top,#f0ad4e 0,#ec971f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f0ad4e),to(#ec971f));background-image:linear-gradient(to bottom,#f0ad4e 0,#ec971f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff0ad4e', endColorstr='#ffec971f', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-o-linear-gradient(top,#d9534f 0,#c9302c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9534f),to(#c9302c));background-image:linear-gradient(to bottom,#d9534f 0,#c9302c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9534f', endColorstr='#ffc9302c', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #286090;background-image:-webkit-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2b669a 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2b669a));background-image:linear-gradient(to bottom,#337ab7 0,#2b669a 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2b669a', GradientType=0);background-repeat:repeat-x;border-color:#2b669a}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-o-linear-gradient(top,#337ab7 0,#2e6da4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#337ab7),to(#2e6da4));background-image:linear-gradient(to bottom,#337ab7 0,#2e6da4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff337ab7', endColorstr='#ff2e6da4', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-o-linear-gradient(top,#dff0d8 0,#d0e9c6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dff0d8),to(#d0e9c6));background-image:linear-gradient(to bottom,#dff0d8 0,#d0e9c6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdff0d8', endColorstr='#ffd0e9c6', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-o-linear-gradient(top,#d9edf7 0,#c4e3f3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#d9edf7),to(#c4e3f3));background-image:linear-gradient(to bottom,#d9edf7 0,#c4e3f3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffd9edf7', endColorstr='#ffc4e3f3', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-o-linear-gradient(top,#fcf8e3 0,#faf2cc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fcf8e3),to(#faf2cc));background-image:linear-gradient(to bottom,#fcf8e3 0,#faf2cc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fffcf8e3', endColorstr='#fffaf2cc', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-o-linear-gradient(top,#f2dede 0,#ebcccc 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f2dede),to(#ebcccc));background-image:linear-gradient(to bottom,#f2dede 0,#ebcccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff2dede', endColorstr='#ffebcccc', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} \ No newline at end of file diff --git a/src/Server/OneTrueError.Web/Content/bootstrap.css b/src/Server/OneTrueError.Web/Content/bootstrap.css deleted file mode 100644 index 680e7687..00000000 --- a/src/Server/OneTrueError.Web/Content/bootstrap.css +++ /dev/null @@ -1,6800 +0,0 @@ -/*! - * Bootstrap v3.3.5 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ -/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ -html { - font-family: sans-serif; - -webkit-text-size-adjust: 100%; - -ms-text-size-adjust: 100%; -} -body { - margin: 0; -} -article, -aside, -details, -figcaption, -figure, -footer, -header, -hgroup, -main, -menu, -nav, -section, -summary { - display: block; -} -audio, -canvas, -progress, -video { - display: inline-block; - vertical-align: baseline; -} -audio:not([controls]) { - display: none; - height: 0; -} -[hidden], -template { - display: none; -} -a { - background-color: transparent; -} -a:active, -a:hover { - outline: 0; -} -abbr[title] { - border-bottom: 1px dotted; -} -b, -strong { - font-weight: bold; -} -dfn { - font-style: italic; -} -h1 { - margin: .67em 0; - font-size: 2em; -} -mark { - color: #000; - background: #ff0; -} -small { - font-size: 80%; -} -sub, -sup { - position: relative; - font-size: 75%; - line-height: 0; - vertical-align: baseline; -} -sup { - top: -.5em; -} -sub { - bottom: -.25em; -} -img { - border: 0; -} -svg:not(:root) { - overflow: hidden; -} -figure { - margin: 1em 40px; -} -hr { - height: 0; - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; -} -pre { - overflow: auto; -} -code, -kbd, -pre, -samp { - font-family: monospace, monospace; - font-size: 1em; -} -button, -input, -optgroup, -select, -textarea { - margin: 0; - font: inherit; - color: inherit; -} -button { - overflow: visible; -} -button, -select { - text-transform: none; -} -button, -html input[type="button"], -input[type="reset"], -input[type="submit"] { - -webkit-appearance: button; - cursor: pointer; -} -button[disabled], -html input[disabled] { - cursor: default; -} -button::-moz-focus-inner, -input::-moz-focus-inner { - padding: 0; - border: 0; -} -input { - line-height: normal; -} -input[type="checkbox"], -input[type="radio"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - padding: 0; -} -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - height: auto; -} -input[type="search"] { - -webkit-box-sizing: content-box; - -moz-box-sizing: content-box; - box-sizing: content-box; - -webkit-appearance: textfield; -} -input[type="search"]::-webkit-search-cancel-button, -input[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} -fieldset { - padding: .35em .625em .75em; - margin: 0 2px; - border: 1px solid #c0c0c0; -} -legend { - padding: 0; - border: 0; -} -textarea { - overflow: auto; -} -optgroup { - font-weight: bold; -} -table { - border-spacing: 0; - border-collapse: collapse; -} -td, -th { - padding: 0; -} -/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ -@media print { - *, - *:before, - *:after { - color: #000 !important; - text-shadow: none !important; - background: transparent !important; - -webkit-box-shadow: none !important; - box-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - a[href^="#"]:after, - a[href^="javascript:"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - - page-break-inside: avoid; - } - thead { - display: table-header-group; - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } - .navbar { - display: none; - } - .btn > .caret, - .dropup > .btn > .caret { - border-top-color: #000 !important; - } - .label { - border: 1px solid #000; - } - .table { - border-collapse: collapse !important; - } - .table td, - .table th { - background-color: #fff !important; - } - .table-bordered th, - .table-bordered td { - border: 1px solid #ddd !important; - } -} -@font-face { - font-family: 'Glyphicons Halflings'; - - src: url('../fonts/glyphicons-halflings-regular.eot'); - src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); -} -.glyphicon { - position: relative; - top: 1px; - display: inline-block; - font-family: 'Glyphicons Halflings'; - font-style: normal; - font-weight: normal; - line-height: 1; - - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.glyphicon-asterisk:before { - content: "\2a"; -} -.glyphicon-plus:before { - content: "\2b"; -} -.glyphicon-euro:before, -.glyphicon-eur:before { - content: "\20ac"; -} -.glyphicon-minus:before { - content: "\2212"; -} -.glyphicon-cloud:before { - content: "\2601"; -} -.glyphicon-envelope:before { - content: "\2709"; -} -.glyphicon-pencil:before { - content: "\270f"; -} -.glyphicon-glass:before { - content: "\e001"; -} -.glyphicon-music:before { - content: "\e002"; -} -.glyphicon-search:before { - content: "\e003"; -} -.glyphicon-heart:before { - content: "\e005"; -} -.glyphicon-star:before { - content: "\e006"; -} -.glyphicon-star-empty:before { - content: "\e007"; -} -.glyphicon-user:before { - content: "\e008"; -} -.glyphicon-film:before { - content: "\e009"; -} -.glyphicon-th-large:before { - content: "\e010"; -} -.glyphicon-th:before { - content: "\e011"; -} -.glyphicon-th-list:before { - content: "\e012"; -} -.glyphicon-ok:before { - content: "\e013"; -} -.glyphicon-remove:before { - content: "\e014"; -} -.glyphicon-zoom-in:before { - content: "\e015"; -} -.glyphicon-zoom-out:before { - content: "\e016"; -} -.glyphicon-off:before { - content: "\e017"; -} -.glyphicon-signal:before { - content: "\e018"; -} -.glyphicon-cog:before { - content: "\e019"; -} -.glyphicon-trash:before { - content: "\e020"; -} -.glyphicon-home:before { - content: "\e021"; -} -.glyphicon-file:before { - content: "\e022"; -} -.glyphicon-time:before { - content: "\e023"; -} -.glyphicon-road:before { - content: "\e024"; -} -.glyphicon-download-alt:before { - content: "\e025"; -} -.glyphicon-download:before { - content: "\e026"; -} -.glyphicon-upload:before { - content: "\e027"; -} -.glyphicon-inbox:before { - content: "\e028"; -} -.glyphicon-play-circle:before { - content: "\e029"; -} -.glyphicon-repeat:before { - content: "\e030"; -} -.glyphicon-refresh:before { - content: "\e031"; -} -.glyphicon-list-alt:before { - content: "\e032"; -} -.glyphicon-lock:before { - content: "\e033"; -} -.glyphicon-flag:before { - content: "\e034"; -} -.glyphicon-headphones:before { - content: "\e035"; -} -.glyphicon-volume-off:before { - content: "\e036"; -} -.glyphicon-volume-down:before { - content: "\e037"; -} -.glyphicon-volume-up:before { - content: "\e038"; -} -.glyphicon-qrcode:before { - content: "\e039"; -} -.glyphicon-barcode:before { - content: "\e040"; -} -.glyphicon-tag:before { - content: "\e041"; -} -.glyphicon-tags:before { - content: "\e042"; -} -.glyphicon-book:before { - content: "\e043"; -} -.glyphicon-bookmark:before { - content: "\e044"; -} -.glyphicon-print:before { - content: "\e045"; -} -.glyphicon-camera:before { - content: "\e046"; -} -.glyphicon-font:before { - content: "\e047"; -} -.glyphicon-bold:before { - content: "\e048"; -} -.glyphicon-italic:before { - content: "\e049"; -} -.glyphicon-text-height:before { - content: "\e050"; -} -.glyphicon-text-width:before { - content: "\e051"; -} -.glyphicon-align-left:before { - content: "\e052"; -} -.glyphicon-align-center:before { - content: "\e053"; -} -.glyphicon-align-right:before { - content: "\e054"; -} -.glyphicon-align-justify:before { - content: "\e055"; -} -.glyphicon-list:before { - content: "\e056"; -} -.glyphicon-indent-left:before { - content: "\e057"; -} -.glyphicon-indent-right:before { - content: "\e058"; -} -.glyphicon-facetime-video:before { - content: "\e059"; -} -.glyphicon-picture:before { - content: "\e060"; -} -.glyphicon-map-marker:before { - content: "\e062"; -} -.glyphicon-adjust:before { - content: "\e063"; -} -.glyphicon-tint:before { - content: "\e064"; -} -.glyphicon-edit:before { - content: "\e065"; -} -.glyphicon-share:before { - content: "\e066"; -} -.glyphicon-check:before { - content: "\e067"; -} -.glyphicon-move:before { - content: "\e068"; -} -.glyphicon-step-backward:before { - content: "\e069"; -} -.glyphicon-fast-backward:before { - content: "\e070"; -} -.glyphicon-backward:before { - content: "\e071"; -} -.glyphicon-play:before { - content: "\e072"; -} -.glyphicon-pause:before { - content: "\e073"; -} -.glyphicon-stop:before { - content: "\e074"; -} -.glyphicon-forward:before { - content: "\e075"; -} -.glyphicon-fast-forward:before { - content: "\e076"; -} -.glyphicon-step-forward:before { - content: "\e077"; -} -.glyphicon-eject:before { - content: "\e078"; -} -.glyphicon-chevron-left:before { - content: "\e079"; -} -.glyphicon-chevron-right:before { - content: "\e080"; -} -.glyphicon-plus-sign:before { - content: "\e081"; -} -.glyphicon-minus-sign:before { - content: "\e082"; -} -.glyphicon-remove-sign:before { - content: "\e083"; -} -.glyphicon-ok-sign:before { - content: "\e084"; -} -.glyphicon-question-sign:before { - content: "\e085"; -} -.glyphicon-info-sign:before { - content: "\e086"; -} -.glyphicon-screenshot:before { - content: "\e087"; -} -.glyphicon-remove-circle:before { - content: "\e088"; -} -.glyphicon-ok-circle:before { - content: "\e089"; -} -.glyphicon-ban-circle:before { - content: "\e090"; -} -.glyphicon-arrow-left:before { - content: "\e091"; -} -.glyphicon-arrow-right:before { - content: "\e092"; -} -.glyphicon-arrow-up:before { - content: "\e093"; -} -.glyphicon-arrow-down:before { - content: "\e094"; -} -.glyphicon-share-alt:before { - content: "\e095"; -} -.glyphicon-resize-full:before { - content: "\e096"; -} -.glyphicon-resize-small:before { - content: "\e097"; -} -.glyphicon-exclamation-sign:before { - content: "\e101"; -} -.glyphicon-gift:before { - content: "\e102"; -} -.glyphicon-leaf:before { - content: "\e103"; -} -.glyphicon-fire:before { - content: "\e104"; -} -.glyphicon-eye-open:before { - content: "\e105"; -} -.glyphicon-eye-close:before { - content: "\e106"; -} -.glyphicon-warning-sign:before { - content: "\e107"; -} -.glyphicon-plane:before { - content: "\e108"; -} -.glyphicon-calendar:before { - content: "\e109"; -} -.glyphicon-random:before { - content: "\e110"; -} -.glyphicon-comment:before { - content: "\e111"; -} -.glyphicon-magnet:before { - content: "\e112"; -} -.glyphicon-chevron-up:before { - content: "\e113"; -} -.glyphicon-chevron-down:before { - content: "\e114"; -} -.glyphicon-retweet:before { - content: "\e115"; -} -.glyphicon-shopping-cart:before { - content: "\e116"; -} -.glyphicon-folder-close:before { - content: "\e117"; -} -.glyphicon-folder-open:before { - content: "\e118"; -} -.glyphicon-resize-vertical:before { - content: "\e119"; -} -.glyphicon-resize-horizontal:before { - content: "\e120"; -} -.glyphicon-hdd:before { - content: "\e121"; -} -.glyphicon-bullhorn:before { - content: "\e122"; -} -.glyphicon-bell:before { - content: "\e123"; -} -.glyphicon-certificate:before { - content: "\e124"; -} -.glyphicon-thumbs-up:before { - content: "\e125"; -} -.glyphicon-thumbs-down:before { - content: "\e126"; -} -.glyphicon-hand-right:before { - content: "\e127"; -} -.glyphicon-hand-left:before { - content: "\e128"; -} -.glyphicon-hand-up:before { - content: "\e129"; -} -.glyphicon-hand-down:before { - content: "\e130"; -} -.glyphicon-circle-arrow-right:before { - content: "\e131"; -} -.glyphicon-circle-arrow-left:before { - content: "\e132"; -} -.glyphicon-circle-arrow-up:before { - content: "\e133"; -} -.glyphicon-circle-arrow-down:before { - content: "\e134"; -} -.glyphicon-globe:before { - content: "\e135"; -} -.glyphicon-wrench:before { - content: "\e136"; -} -.glyphicon-tasks:before { - content: "\e137"; -} -.glyphicon-filter:before { - content: "\e138"; -} -.glyphicon-briefcase:before { - content: "\e139"; -} -.glyphicon-fullscreen:before { - content: "\e140"; -} -.glyphicon-dashboard:before { - content: "\e141"; -} -.glyphicon-paperclip:before { - content: "\e142"; -} -.glyphicon-heart-empty:before { - content: "\e143"; -} -.glyphicon-link:before { - content: "\e144"; -} -.glyphicon-phone:before { - content: "\e145"; -} -.glyphicon-pushpin:before { - content: "\e146"; -} -.glyphicon-usd:before { - content: "\e148"; -} -.glyphicon-gbp:before { - content: "\e149"; -} -.glyphicon-sort:before { - content: "\e150"; -} -.glyphicon-sort-by-alphabet:before { - content: "\e151"; -} -.glyphicon-sort-by-alphabet-alt:before { - content: "\e152"; -} -.glyphicon-sort-by-order:before { - content: "\e153"; -} -.glyphicon-sort-by-order-alt:before { - content: "\e154"; -} -.glyphicon-sort-by-attributes:before { - content: "\e155"; -} -.glyphicon-sort-by-attributes-alt:before { - content: "\e156"; -} -.glyphicon-unchecked:before { - content: "\e157"; -} -.glyphicon-expand:before { - content: "\e158"; -} -.glyphicon-collapse-down:before { - content: "\e159"; -} -.glyphicon-collapse-up:before { - content: "\e160"; -} -.glyphicon-log-in:before { - content: "\e161"; -} -.glyphicon-flash:before { - content: "\e162"; -} -.glyphicon-log-out:before { - content: "\e163"; -} -.glyphicon-new-window:before { - content: "\e164"; -} -.glyphicon-record:before { - content: "\e165"; -} -.glyphicon-save:before { - content: "\e166"; -} -.glyphicon-open:before { - content: "\e167"; -} -.glyphicon-saved:before { - content: "\e168"; -} -.glyphicon-import:before { - content: "\e169"; -} -.glyphicon-export:before { - content: "\e170"; -} -.glyphicon-send:before { - content: "\e171"; -} -.glyphicon-floppy-disk:before { - content: "\e172"; -} -.glyphicon-floppy-saved:before { - content: "\e173"; -} -.glyphicon-floppy-remove:before { - content: "\e174"; -} -.glyphicon-floppy-save:before { - content: "\e175"; -} -.glyphicon-floppy-open:before { - content: "\e176"; -} -.glyphicon-credit-card:before { - content: "\e177"; -} -.glyphicon-transfer:before { - content: "\e178"; -} -.glyphicon-cutlery:before { - content: "\e179"; -} -.glyphicon-header:before { - content: "\e180"; -} -.glyphicon-compressed:before { - content: "\e181"; -} -.glyphicon-earphone:before { - content: "\e182"; -} -.glyphicon-phone-alt:before { - content: "\e183"; -} -.glyphicon-tower:before { - content: "\e184"; -} -.glyphicon-stats:before { - content: "\e185"; -} -.glyphicon-sd-video:before { - content: "\e186"; -} -.glyphicon-hd-video:before { - content: "\e187"; -} -.glyphicon-subtitles:before { - content: "\e188"; -} -.glyphicon-sound-stereo:before { - content: "\e189"; -} -.glyphicon-sound-dolby:before { - content: "\e190"; -} -.glyphicon-sound-5-1:before { - content: "\e191"; -} -.glyphicon-sound-6-1:before { - content: "\e192"; -} -.glyphicon-sound-7-1:before { - content: "\e193"; -} -.glyphicon-copyright-mark:before { - content: "\e194"; -} -.glyphicon-registration-mark:before { - content: "\e195"; -} -.glyphicon-cloud-download:before { - content: "\e197"; -} -.glyphicon-cloud-upload:before { - content: "\e198"; -} -.glyphicon-tree-conifer:before { - content: "\e199"; -} -.glyphicon-tree-deciduous:before { - content: "\e200"; -} -.glyphicon-cd:before { - content: "\e201"; -} -.glyphicon-save-file:before { - content: "\e202"; -} -.glyphicon-open-file:before { - content: "\e203"; -} -.glyphicon-level-up:before { - content: "\e204"; -} -.glyphicon-copy:before { - content: "\e205"; -} -.glyphicon-paste:before { - content: "\e206"; -} -.glyphicon-alert:before { - content: "\e209"; -} -.glyphicon-equalizer:before { - content: "\e210"; -} -.glyphicon-king:before { - content: "\e211"; -} -.glyphicon-queen:before { - content: "\e212"; -} -.glyphicon-pawn:before { - content: "\e213"; -} -.glyphicon-bishop:before { - content: "\e214"; -} -.glyphicon-knight:before { - content: "\e215"; -} -.glyphicon-baby-formula:before { - content: "\e216"; -} -.glyphicon-tent:before { - content: "\26fa"; -} -.glyphicon-blackboard:before { - content: "\e218"; -} -.glyphicon-bed:before { - content: "\e219"; -} -.glyphicon-apple:before { - content: "\f8ff"; -} -.glyphicon-erase:before { - content: "\e221"; -} -.glyphicon-hourglass:before { - content: "\231b"; -} -.glyphicon-lamp:before { - content: "\e223"; -} -.glyphicon-duplicate:before { - content: "\e224"; -} -.glyphicon-piggy-bank:before { - content: "\e225"; -} -.glyphicon-scissors:before { - content: "\e226"; -} -.glyphicon-bitcoin:before { - content: "\e227"; -} -.glyphicon-btc:before { - content: "\e227"; -} -.glyphicon-xbt:before { - content: "\e227"; -} -.glyphicon-yen:before { - content: "\00a5"; -} -.glyphicon-jpy:before { - content: "\00a5"; -} -.glyphicon-ruble:before { - content: "\20bd"; -} -.glyphicon-rub:before { - content: "\20bd"; -} -.glyphicon-scale:before { - content: "\e230"; -} -.glyphicon-ice-lolly:before { - content: "\e231"; -} -.glyphicon-ice-lolly-tasted:before { - content: "\e232"; -} -.glyphicon-education:before { - content: "\e233"; -} -.glyphicon-option-horizontal:before { - content: "\e234"; -} -.glyphicon-option-vertical:before { - content: "\e235"; -} -.glyphicon-menu-hamburger:before { - content: "\e236"; -} -.glyphicon-modal-window:before { - content: "\e237"; -} -.glyphicon-oil:before { - content: "\e238"; -} -.glyphicon-grain:before { - content: "\e239"; -} -.glyphicon-sunglasses:before { - content: "\e240"; -} -.glyphicon-text-size:before { - content: "\e241"; -} -.glyphicon-text-color:before { - content: "\e242"; -} -.glyphicon-text-background:before { - content: "\e243"; -} -.glyphicon-object-align-top:before { - content: "\e244"; -} -.glyphicon-object-align-bottom:before { - content: "\e245"; -} -.glyphicon-object-align-horizontal:before { - content: "\e246"; -} -.glyphicon-object-align-left:before { - content: "\e247"; -} -.glyphicon-object-align-vertical:before { - content: "\e248"; -} -.glyphicon-object-align-right:before { - content: "\e249"; -} -.glyphicon-triangle-right:before { - content: "\e250"; -} -.glyphicon-triangle-left:before { - content: "\e251"; -} -.glyphicon-triangle-bottom:before { - content: "\e252"; -} -.glyphicon-triangle-top:before { - content: "\e253"; -} -.glyphicon-console:before { - content: "\e254"; -} -.glyphicon-superscript:before { - content: "\e255"; -} -.glyphicon-subscript:before { - content: "\e256"; -} -.glyphicon-menu-left:before { - content: "\e257"; -} -.glyphicon-menu-right:before { - content: "\e258"; -} -.glyphicon-menu-down:before { - content: "\e259"; -} -.glyphicon-menu-up:before { - content: "\e260"; -} -* { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -*:before, -*:after { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -html { - font-size: 10px; - - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); -} -body { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - line-height: 1.42857143; - color: #333; - background-color: #fff; -} -input, -button, -select, -textarea { - font-family: inherit; - font-size: inherit; - line-height: inherit; -} -a { - color: #337ab7; - text-decoration: none; -} -a:hover, -a:focus { - color: #23527c; - text-decoration: underline; -} -a:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -figure { - margin: 0; -} -img { - vertical-align: middle; -} -.img-responsive, -.thumbnail > img, -.thumbnail a > img, -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - display: block; - max-width: 100%; - height: auto; -} -.img-rounded { - border-radius: 6px; -} -.img-thumbnail { - display: inline-block; - max-width: 100%; - height: auto; - padding: 4px; - line-height: 1.42857143; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 4px; - -webkit-transition: all .2s ease-in-out; - -o-transition: all .2s ease-in-out; - transition: all .2s ease-in-out; -} -.img-circle { - border-radius: 50%; -} -hr { - margin-top: 20px; - margin-bottom: 20px; - border: 0; - border-top: 1px solid #eee; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} -[role="button"] { - cursor: pointer; -} -h1, -h2, -h3, -h4, -h5, -h6, -.h1, -.h2, -.h3, -.h4, -.h5, -.h6 { - font-family: inherit; - font-weight: 500; - line-height: 1.1; - color: inherit; -} -h1 small, -h2 small, -h3 small, -h4 small, -h5 small, -h6 small, -.h1 small, -.h2 small, -.h3 small, -.h4 small, -.h5 small, -.h6 small, -h1 .small, -h2 .small, -h3 .small, -h4 .small, -h5 .small, -h6 .small, -.h1 .small, -.h2 .small, -.h3 .small, -.h4 .small, -.h5 .small, -.h6 .small { - font-weight: normal; - line-height: 1; - color: #777; -} -h1, -.h1, -h2, -.h2, -h3, -.h3 { - margin-top: 20px; - margin-bottom: 10px; -} -h1 small, -.h1 small, -h2 small, -.h2 small, -h3 small, -.h3 small, -h1 .small, -.h1 .small, -h2 .small, -.h2 .small, -h3 .small, -.h3 .small { - font-size: 65%; -} -h4, -.h4, -h5, -.h5, -h6, -.h6 { - margin-top: 10px; - margin-bottom: 10px; -} -h4 small, -.h4 small, -h5 small, -.h5 small, -h6 small, -.h6 small, -h4 .small, -.h4 .small, -h5 .small, -.h5 .small, -h6 .small, -.h6 .small { - font-size: 75%; -} -h1, -.h1 { - font-size: 36px; -} -h2, -.h2 { - font-size: 30px; -} -h3, -.h3 { - font-size: 24px; -} -h4, -.h4 { - font-size: 18px; -} -h5, -.h5 { - font-size: 14px; -} -h6, -.h6 { - font-size: 12px; -} -p { - margin: 0 0 10px; -} -.lead { - margin-bottom: 20px; - font-size: 16px; - font-weight: 300; - line-height: 1.4; -} -@media (min-width: 768px) { - .lead { - font-size: 21px; - } -} -small, -.small { - font-size: 85%; -} -mark, -.mark { - padding: .2em; - background-color: #fcf8e3; -} -.text-left { - text-align: left; -} -.text-right { - text-align: right; -} -.text-center { - text-align: center; -} -.text-justify { - text-align: justify; -} -.text-nowrap { - white-space: nowrap; -} -.text-lowercase { - text-transform: lowercase; -} -.text-uppercase { - text-transform: uppercase; -} -.text-capitalize { - text-transform: capitalize; -} -.text-muted { - color: #777; -} -.text-primary { - color: #337ab7; -} -a.text-primary:hover, -a.text-primary:focus { - color: #286090; -} -.text-success { - color: #3c763d; -} -a.text-success:hover, -a.text-success:focus { - color: #2b542c; -} -.text-info { - color: #31708f; -} -a.text-info:hover, -a.text-info:focus { - color: #245269; -} -.text-warning { - color: #8a6d3b; -} -a.text-warning:hover, -a.text-warning:focus { - color: #66512c; -} -.text-danger { - color: #a94442; -} -a.text-danger:hover, -a.text-danger:focus { - color: #843534; -} -.bg-primary { - color: #fff; - background-color: #337ab7; -} -a.bg-primary:hover, -a.bg-primary:focus { - background-color: #286090; -} -.bg-success { - background-color: #dff0d8; -} -a.bg-success:hover, -a.bg-success:focus { - background-color: #c1e2b3; -} -.bg-info { - background-color: #d9edf7; -} -a.bg-info:hover, -a.bg-info:focus { - background-color: #afd9ee; -} -.bg-warning { - background-color: #fcf8e3; -} -a.bg-warning:hover, -a.bg-warning:focus { - background-color: #f7ecb5; -} -.bg-danger { - background-color: #f2dede; -} -a.bg-danger:hover, -a.bg-danger:focus { - background-color: #e4b9b9; -} -.page-header { - padding-bottom: 9px; - margin: 40px 0 20px; - border-bottom: 1px solid #eee; -} -ul, -ol { - margin-top: 0; - margin-bottom: 10px; -} -ul ul, -ol ul, -ul ol, -ol ol { - margin-bottom: 0; -} -.list-unstyled { - padding-left: 0; - list-style: none; -} -.list-inline { - padding-left: 0; - margin-left: -5px; - list-style: none; -} -.list-inline > li { - display: inline-block; - padding-right: 5px; - padding-left: 5px; -} -dl { - margin-top: 0; - margin-bottom: 20px; -} -dt, -dd { - line-height: 1.42857143; -} -dt { - font-weight: bold; -} -dd { - margin-left: 0; -} -@media (min-width: 768px) { - .dl-horizontal dt { - float: left; - width: 160px; - overflow: hidden; - clear: left; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; - } - .dl-horizontal dd { - margin-left: 180px; - } -} -abbr[title], -abbr[data-original-title] { - cursor: help; - border-bottom: 1px dotted #777; -} -.initialism { - font-size: 90%; - text-transform: uppercase; -} -blockquote { - padding: 10px 20px; - margin: 0 0 20px; - font-size: 17.5px; - border-left: 5px solid #eee; -} -blockquote p:last-child, -blockquote ul:last-child, -blockquote ol:last-child { - margin-bottom: 0; -} -blockquote footer, -blockquote small, -blockquote .small { - display: block; - font-size: 80%; - line-height: 1.42857143; - color: #777; -} -blockquote footer:before, -blockquote small:before, -blockquote .small:before { - content: '\2014 \00A0'; -} -.blockquote-reverse, -blockquote.pull-right { - padding-right: 15px; - padding-left: 0; - text-align: right; - border-right: 5px solid #eee; - border-left: 0; -} -.blockquote-reverse footer:before, -blockquote.pull-right footer:before, -.blockquote-reverse small:before, -blockquote.pull-right small:before, -.blockquote-reverse .small:before, -blockquote.pull-right .small:before { - content: ''; -} -.blockquote-reverse footer:after, -blockquote.pull-right footer:after, -.blockquote-reverse small:after, -blockquote.pull-right small:after, -.blockquote-reverse .small:after, -blockquote.pull-right .small:after { - content: '\00A0 \2014'; -} -address { - margin-bottom: 20px; - font-style: normal; - line-height: 1.42857143; -} -code, -kbd, -pre, -samp { - font-family: Menlo, Monaco, Consolas, "Courier New", monospace; -} -code { - padding: 2px 4px; - font-size: 90%; - color: #c7254e; - background-color: #f9f2f4; - border-radius: 4px; -} -kbd { - padding: 2px 4px; - font-size: 90%; - color: #fff; - background-color: #333; - border-radius: 3px; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); -} -kbd kbd { - padding: 0; - font-size: 100%; - font-weight: bold; - -webkit-box-shadow: none; - box-shadow: none; -} -pre { - display: block; - padding: 9.5px; - margin: 0 0 10px; - font-size: 13px; - line-height: 1.42857143; - color: #333; - word-break: break-all; - word-wrap: break-word; - background-color: #f5f5f5; - border: 1px solid #ccc; - border-radius: 4px; -} -pre code { - padding: 0; - font-size: inherit; - color: inherit; - white-space: pre-wrap; - background-color: transparent; - border-radius: 0; -} -.pre-scrollable { - max-height: 340px; - overflow-y: scroll; -} -.container { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; -} -@media (min-width: 768px) { - .container { - width: 750px; - } -} -@media (min-width: 992px) { - .container { - width: 970px; - } -} -@media (min-width: 1200px) { - .container { - width: 1170px; - } -} -.container-fluid { - padding-right: 15px; - padding-left: 15px; - margin-right: auto; - margin-left: auto; -} -.row { - margin-right: -15px; - margin-left: -15px; -} -.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { - position: relative; - min-height: 1px; - padding-right: 15px; - padding-left: 15px; -} -.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { - float: left; -} -.col-xs-12 { - width: 100%; -} -.col-xs-11 { - width: 91.66666667%; -} -.col-xs-10 { - width: 83.33333333%; -} -.col-xs-9 { - width: 75%; -} -.col-xs-8 { - width: 66.66666667%; -} -.col-xs-7 { - width: 58.33333333%; -} -.col-xs-6 { - width: 50%; -} -.col-xs-5 { - width: 41.66666667%; -} -.col-xs-4 { - width: 33.33333333%; -} -.col-xs-3 { - width: 25%; -} -.col-xs-2 { - width: 16.66666667%; -} -.col-xs-1 { - width: 8.33333333%; -} -.col-xs-pull-12 { - right: 100%; -} -.col-xs-pull-11 { - right: 91.66666667%; -} -.col-xs-pull-10 { - right: 83.33333333%; -} -.col-xs-pull-9 { - right: 75%; -} -.col-xs-pull-8 { - right: 66.66666667%; -} -.col-xs-pull-7 { - right: 58.33333333%; -} -.col-xs-pull-6 { - right: 50%; -} -.col-xs-pull-5 { - right: 41.66666667%; -} -.col-xs-pull-4 { - right: 33.33333333%; -} -.col-xs-pull-3 { - right: 25%; -} -.col-xs-pull-2 { - right: 16.66666667%; -} -.col-xs-pull-1 { - right: 8.33333333%; -} -.col-xs-pull-0 { - right: auto; -} -.col-xs-push-12 { - left: 100%; -} -.col-xs-push-11 { - left: 91.66666667%; -} -.col-xs-push-10 { - left: 83.33333333%; -} -.col-xs-push-9 { - left: 75%; -} -.col-xs-push-8 { - left: 66.66666667%; -} -.col-xs-push-7 { - left: 58.33333333%; -} -.col-xs-push-6 { - left: 50%; -} -.col-xs-push-5 { - left: 41.66666667%; -} -.col-xs-push-4 { - left: 33.33333333%; -} -.col-xs-push-3 { - left: 25%; -} -.col-xs-push-2 { - left: 16.66666667%; -} -.col-xs-push-1 { - left: 8.33333333%; -} -.col-xs-push-0 { - left: auto; -} -.col-xs-offset-12 { - margin-left: 100%; -} -.col-xs-offset-11 { - margin-left: 91.66666667%; -} -.col-xs-offset-10 { - margin-left: 83.33333333%; -} -.col-xs-offset-9 { - margin-left: 75%; -} -.col-xs-offset-8 { - margin-left: 66.66666667%; -} -.col-xs-offset-7 { - margin-left: 58.33333333%; -} -.col-xs-offset-6 { - margin-left: 50%; -} -.col-xs-offset-5 { - margin-left: 41.66666667%; -} -.col-xs-offset-4 { - margin-left: 33.33333333%; -} -.col-xs-offset-3 { - margin-left: 25%; -} -.col-xs-offset-2 { - margin-left: 16.66666667%; -} -.col-xs-offset-1 { - margin-left: 8.33333333%; -} -.col-xs-offset-0 { - margin-left: 0; -} -@media (min-width: 768px) { - .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { - float: left; - } - .col-sm-12 { - width: 100%; - } - .col-sm-11 { - width: 91.66666667%; - } - .col-sm-10 { - width: 83.33333333%; - } - .col-sm-9 { - width: 75%; - } - .col-sm-8 { - width: 66.66666667%; - } - .col-sm-7 { - width: 58.33333333%; - } - .col-sm-6 { - width: 50%; - } - .col-sm-5 { - width: 41.66666667%; - } - .col-sm-4 { - width: 33.33333333%; - } - .col-sm-3 { - width: 25%; - } - .col-sm-2 { - width: 16.66666667%; - } - .col-sm-1 { - width: 8.33333333%; - } - .col-sm-pull-12 { - right: 100%; - } - .col-sm-pull-11 { - right: 91.66666667%; - } - .col-sm-pull-10 { - right: 83.33333333%; - } - .col-sm-pull-9 { - right: 75%; - } - .col-sm-pull-8 { - right: 66.66666667%; - } - .col-sm-pull-7 { - right: 58.33333333%; - } - .col-sm-pull-6 { - right: 50%; - } - .col-sm-pull-5 { - right: 41.66666667%; - } - .col-sm-pull-4 { - right: 33.33333333%; - } - .col-sm-pull-3 { - right: 25%; - } - .col-sm-pull-2 { - right: 16.66666667%; - } - .col-sm-pull-1 { - right: 8.33333333%; - } - .col-sm-pull-0 { - right: auto; - } - .col-sm-push-12 { - left: 100%; - } - .col-sm-push-11 { - left: 91.66666667%; - } - .col-sm-push-10 { - left: 83.33333333%; - } - .col-sm-push-9 { - left: 75%; - } - .col-sm-push-8 { - left: 66.66666667%; - } - .col-sm-push-7 { - left: 58.33333333%; - } - .col-sm-push-6 { - left: 50%; - } - .col-sm-push-5 { - left: 41.66666667%; - } - .col-sm-push-4 { - left: 33.33333333%; - } - .col-sm-push-3 { - left: 25%; - } - .col-sm-push-2 { - left: 16.66666667%; - } - .col-sm-push-1 { - left: 8.33333333%; - } - .col-sm-push-0 { - left: auto; - } - .col-sm-offset-12 { - margin-left: 100%; - } - .col-sm-offset-11 { - margin-left: 91.66666667%; - } - .col-sm-offset-10 { - margin-left: 83.33333333%; - } - .col-sm-offset-9 { - margin-left: 75%; - } - .col-sm-offset-8 { - margin-left: 66.66666667%; - } - .col-sm-offset-7 { - margin-left: 58.33333333%; - } - .col-sm-offset-6 { - margin-left: 50%; - } - .col-sm-offset-5 { - margin-left: 41.66666667%; - } - .col-sm-offset-4 { - margin-left: 33.33333333%; - } - .col-sm-offset-3 { - margin-left: 25%; - } - .col-sm-offset-2 { - margin-left: 16.66666667%; - } - .col-sm-offset-1 { - margin-left: 8.33333333%; - } - .col-sm-offset-0 { - margin-left: 0; - } -} -@media (min-width: 992px) { - .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { - float: left; - } - .col-md-12 { - width: 100%; - } - .col-md-11 { - width: 91.66666667%; - } - .col-md-10 { - width: 83.33333333%; - } - .col-md-9 { - width: 75%; - } - .col-md-8 { - width: 66.66666667%; - } - .col-md-7 { - width: 58.33333333%; - } - .col-md-6 { - width: 50%; - } - .col-md-5 { - width: 41.66666667%; - } - .col-md-4 { - width: 33.33333333%; - } - .col-md-3 { - width: 25%; - } - .col-md-2 { - width: 16.66666667%; - } - .col-md-1 { - width: 8.33333333%; - } - .col-md-pull-12 { - right: 100%; - } - .col-md-pull-11 { - right: 91.66666667%; - } - .col-md-pull-10 { - right: 83.33333333%; - } - .col-md-pull-9 { - right: 75%; - } - .col-md-pull-8 { - right: 66.66666667%; - } - .col-md-pull-7 { - right: 58.33333333%; - } - .col-md-pull-6 { - right: 50%; - } - .col-md-pull-5 { - right: 41.66666667%; - } - .col-md-pull-4 { - right: 33.33333333%; - } - .col-md-pull-3 { - right: 25%; - } - .col-md-pull-2 { - right: 16.66666667%; - } - .col-md-pull-1 { - right: 8.33333333%; - } - .col-md-pull-0 { - right: auto; - } - .col-md-push-12 { - left: 100%; - } - .col-md-push-11 { - left: 91.66666667%; - } - .col-md-push-10 { - left: 83.33333333%; - } - .col-md-push-9 { - left: 75%; - } - .col-md-push-8 { - left: 66.66666667%; - } - .col-md-push-7 { - left: 58.33333333%; - } - .col-md-push-6 { - left: 50%; - } - .col-md-push-5 { - left: 41.66666667%; - } - .col-md-push-4 { - left: 33.33333333%; - } - .col-md-push-3 { - left: 25%; - } - .col-md-push-2 { - left: 16.66666667%; - } - .col-md-push-1 { - left: 8.33333333%; - } - .col-md-push-0 { - left: auto; - } - .col-md-offset-12 { - margin-left: 100%; - } - .col-md-offset-11 { - margin-left: 91.66666667%; - } - .col-md-offset-10 { - margin-left: 83.33333333%; - } - .col-md-offset-9 { - margin-left: 75%; - } - .col-md-offset-8 { - margin-left: 66.66666667%; - } - .col-md-offset-7 { - margin-left: 58.33333333%; - } - .col-md-offset-6 { - margin-left: 50%; - } - .col-md-offset-5 { - margin-left: 41.66666667%; - } - .col-md-offset-4 { - margin-left: 33.33333333%; - } - .col-md-offset-3 { - margin-left: 25%; - } - .col-md-offset-2 { - margin-left: 16.66666667%; - } - .col-md-offset-1 { - margin-left: 8.33333333%; - } - .col-md-offset-0 { - margin-left: 0; - } -} -@media (min-width: 1200px) { - .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { - float: left; - } - .col-lg-12 { - width: 100%; - } - .col-lg-11 { - width: 91.66666667%; - } - .col-lg-10 { - width: 83.33333333%; - } - .col-lg-9 { - width: 75%; - } - .col-lg-8 { - width: 66.66666667%; - } - .col-lg-7 { - width: 58.33333333%; - } - .col-lg-6 { - width: 50%; - } - .col-lg-5 { - width: 41.66666667%; - } - .col-lg-4 { - width: 33.33333333%; - } - .col-lg-3 { - width: 25%; - } - .col-lg-2 { - width: 16.66666667%; - } - .col-lg-1 { - width: 8.33333333%; - } - .col-lg-pull-12 { - right: 100%; - } - .col-lg-pull-11 { - right: 91.66666667%; - } - .col-lg-pull-10 { - right: 83.33333333%; - } - .col-lg-pull-9 { - right: 75%; - } - .col-lg-pull-8 { - right: 66.66666667%; - } - .col-lg-pull-7 { - right: 58.33333333%; - } - .col-lg-pull-6 { - right: 50%; - } - .col-lg-pull-5 { - right: 41.66666667%; - } - .col-lg-pull-4 { - right: 33.33333333%; - } - .col-lg-pull-3 { - right: 25%; - } - .col-lg-pull-2 { - right: 16.66666667%; - } - .col-lg-pull-1 { - right: 8.33333333%; - } - .col-lg-pull-0 { - right: auto; - } - .col-lg-push-12 { - left: 100%; - } - .col-lg-push-11 { - left: 91.66666667%; - } - .col-lg-push-10 { - left: 83.33333333%; - } - .col-lg-push-9 { - left: 75%; - } - .col-lg-push-8 { - left: 66.66666667%; - } - .col-lg-push-7 { - left: 58.33333333%; - } - .col-lg-push-6 { - left: 50%; - } - .col-lg-push-5 { - left: 41.66666667%; - } - .col-lg-push-4 { - left: 33.33333333%; - } - .col-lg-push-3 { - left: 25%; - } - .col-lg-push-2 { - left: 16.66666667%; - } - .col-lg-push-1 { - left: 8.33333333%; - } - .col-lg-push-0 { - left: auto; - } - .col-lg-offset-12 { - margin-left: 100%; - } - .col-lg-offset-11 { - margin-left: 91.66666667%; - } - .col-lg-offset-10 { - margin-left: 83.33333333%; - } - .col-lg-offset-9 { - margin-left: 75%; - } - .col-lg-offset-8 { - margin-left: 66.66666667%; - } - .col-lg-offset-7 { - margin-left: 58.33333333%; - } - .col-lg-offset-6 { - margin-left: 50%; - } - .col-lg-offset-5 { - margin-left: 41.66666667%; - } - .col-lg-offset-4 { - margin-left: 33.33333333%; - } - .col-lg-offset-3 { - margin-left: 25%; - } - .col-lg-offset-2 { - margin-left: 16.66666667%; - } - .col-lg-offset-1 { - margin-left: 8.33333333%; - } - .col-lg-offset-0 { - margin-left: 0; - } -} -table { - background-color: transparent; -} -caption { - padding-top: 8px; - padding-bottom: 8px; - color: #777; - text-align: left; -} -th { - text-align: left; -} -.table { - width: 100%; - max-width: 100%; - margin-bottom: 20px; -} -.table > thead > tr > th, -.table > tbody > tr > th, -.table > tfoot > tr > th, -.table > thead > tr > td, -.table > tbody > tr > td, -.table > tfoot > tr > td { - padding: 8px; - line-height: 1.42857143; - vertical-align: top; - border-top: 1px solid #ddd; -} -.table > thead > tr > th { - vertical-align: bottom; - border-bottom: 2px solid #ddd; -} -.table > caption + thead > tr:first-child > th, -.table > colgroup + thead > tr:first-child > th, -.table > thead:first-child > tr:first-child > th, -.table > caption + thead > tr:first-child > td, -.table > colgroup + thead > tr:first-child > td, -.table > thead:first-child > tr:first-child > td { - border-top: 0; -} -.table > tbody + tbody { - border-top: 2px solid #ddd; -} -.table .table { - background-color: #fff; -} -.table-condensed > thead > tr > th, -.table-condensed > tbody > tr > th, -.table-condensed > tfoot > tr > th, -.table-condensed > thead > tr > td, -.table-condensed > tbody > tr > td, -.table-condensed > tfoot > tr > td { - padding: 5px; -} -.table-bordered { - border: 1px solid #ddd; -} -.table-bordered > thead > tr > th, -.table-bordered > tbody > tr > th, -.table-bordered > tfoot > tr > th, -.table-bordered > thead > tr > td, -.table-bordered > tbody > tr > td, -.table-bordered > tfoot > tr > td { - border: 1px solid #ddd; -} -.table-bordered > thead > tr > th, -.table-bordered > thead > tr > td { - border-bottom-width: 2px; -} -.table-striped > tbody > tr:nth-of-type(odd) { - background-color: #f9f9f9; -} -.table-hover > tbody > tr:hover { - background-color: #f5f5f5; -} -table col[class*="col-"] { - position: static; - display: table-column; - float: none; -} -table td[class*="col-"], -table th[class*="col-"] { - position: static; - display: table-cell; - float: none; -} -.table > thead > tr > td.active, -.table > tbody > tr > td.active, -.table > tfoot > tr > td.active, -.table > thead > tr > th.active, -.table > tbody > tr > th.active, -.table > tfoot > tr > th.active, -.table > thead > tr.active > td, -.table > tbody > tr.active > td, -.table > tfoot > tr.active > td, -.table > thead > tr.active > th, -.table > tbody > tr.active > th, -.table > tfoot > tr.active > th { - background-color: #f5f5f5; -} -.table-hover > tbody > tr > td.active:hover, -.table-hover > tbody > tr > th.active:hover, -.table-hover > tbody > tr.active:hover > td, -.table-hover > tbody > tr:hover > .active, -.table-hover > tbody > tr.active:hover > th { - background-color: #e8e8e8; -} -.table > thead > tr > td.success, -.table > tbody > tr > td.success, -.table > tfoot > tr > td.success, -.table > thead > tr > th.success, -.table > tbody > tr > th.success, -.table > tfoot > tr > th.success, -.table > thead > tr.success > td, -.table > tbody > tr.success > td, -.table > tfoot > tr.success > td, -.table > thead > tr.success > th, -.table > tbody > tr.success > th, -.table > tfoot > tr.success > th { - background-color: #dff0d8; -} -.table-hover > tbody > tr > td.success:hover, -.table-hover > tbody > tr > th.success:hover, -.table-hover > tbody > tr.success:hover > td, -.table-hover > tbody > tr:hover > .success, -.table-hover > tbody > tr.success:hover > th { - background-color: #d0e9c6; -} -.table > thead > tr > td.info, -.table > tbody > tr > td.info, -.table > tfoot > tr > td.info, -.table > thead > tr > th.info, -.table > tbody > tr > th.info, -.table > tfoot > tr > th.info, -.table > thead > tr.info > td, -.table > tbody > tr.info > td, -.table > tfoot > tr.info > td, -.table > thead > tr.info > th, -.table > tbody > tr.info > th, -.table > tfoot > tr.info > th { - background-color: #d9edf7; -} -.table-hover > tbody > tr > td.info:hover, -.table-hover > tbody > tr > th.info:hover, -.table-hover > tbody > tr.info:hover > td, -.table-hover > tbody > tr:hover > .info, -.table-hover > tbody > tr.info:hover > th { - background-color: #c4e3f3; -} -.table > thead > tr > td.warning, -.table > tbody > tr > td.warning, -.table > tfoot > tr > td.warning, -.table > thead > tr > th.warning, -.table > tbody > tr > th.warning, -.table > tfoot > tr > th.warning, -.table > thead > tr.warning > td, -.table > tbody > tr.warning > td, -.table > tfoot > tr.warning > td, -.table > thead > tr.warning > th, -.table > tbody > tr.warning > th, -.table > tfoot > tr.warning > th { - background-color: #fcf8e3; -} -.table-hover > tbody > tr > td.warning:hover, -.table-hover > tbody > tr > th.warning:hover, -.table-hover > tbody > tr.warning:hover > td, -.table-hover > tbody > tr:hover > .warning, -.table-hover > tbody > tr.warning:hover > th { - background-color: #faf2cc; -} -.table > thead > tr > td.danger, -.table > tbody > tr > td.danger, -.table > tfoot > tr > td.danger, -.table > thead > tr > th.danger, -.table > tbody > tr > th.danger, -.table > tfoot > tr > th.danger, -.table > thead > tr.danger > td, -.table > tbody > tr.danger > td, -.table > tfoot > tr.danger > td, -.table > thead > tr.danger > th, -.table > tbody > tr.danger > th, -.table > tfoot > tr.danger > th { - background-color: #f2dede; -} -.table-hover > tbody > tr > td.danger:hover, -.table-hover > tbody > tr > th.danger:hover, -.table-hover > tbody > tr.danger:hover > td, -.table-hover > tbody > tr:hover > .danger, -.table-hover > tbody > tr.danger:hover > th { - background-color: #ebcccc; -} -.table-responsive { - min-height: .01%; - overflow-x: auto; -} -@media screen and (max-width: 767px) { - .table-responsive { - width: 100%; - margin-bottom: 15px; - overflow-y: hidden; - -ms-overflow-style: -ms-autohiding-scrollbar; - border: 1px solid #ddd; - } - .table-responsive > .table { - margin-bottom: 0; - } - .table-responsive > .table > thead > tr > th, - .table-responsive > .table > tbody > tr > th, - .table-responsive > .table > tfoot > tr > th, - .table-responsive > .table > thead > tr > td, - .table-responsive > .table > tbody > tr > td, - .table-responsive > .table > tfoot > tr > td { - white-space: nowrap; - } - .table-responsive > .table-bordered { - border: 0; - } - .table-responsive > .table-bordered > thead > tr > th:first-child, - .table-responsive > .table-bordered > tbody > tr > th:first-child, - .table-responsive > .table-bordered > tfoot > tr > th:first-child, - .table-responsive > .table-bordered > thead > tr > td:first-child, - .table-responsive > .table-bordered > tbody > tr > td:first-child, - .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; - } - .table-responsive > .table-bordered > thead > tr > th:last-child, - .table-responsive > .table-bordered > tbody > tr > th:last-child, - .table-responsive > .table-bordered > tfoot > tr > th:last-child, - .table-responsive > .table-bordered > thead > tr > td:last-child, - .table-responsive > .table-bordered > tbody > tr > td:last-child, - .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; - } - .table-responsive > .table-bordered > tbody > tr:last-child > th, - .table-responsive > .table-bordered > tfoot > tr:last-child > th, - .table-responsive > .table-bordered > tbody > tr:last-child > td, - .table-responsive > .table-bordered > tfoot > tr:last-child > td { - border-bottom: 0; - } -} -fieldset { - min-width: 0; - padding: 0; - margin: 0; - border: 0; -} -legend { - display: block; - width: 100%; - padding: 0; - margin-bottom: 20px; - font-size: 21px; - line-height: inherit; - color: #333; - border: 0; - border-bottom: 1px solid #e5e5e5; -} -label { - display: inline-block; - max-width: 100%; - margin-bottom: 5px; - font-weight: bold; -} -input[type="search"] { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -input[type="radio"], -input[type="checkbox"] { - margin: 4px 0 0; - margin-top: 1px \9; - line-height: normal; -} -input[type="file"] { - display: block; -} -input[type="range"] { - display: block; - width: 100%; -} -select[multiple], -select[size] { - height: auto; -} -input[type="file"]:focus, -input[type="radio"]:focus, -input[type="checkbox"]:focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -output { - display: block; - padding-top: 7px; - font-size: 14px; - line-height: 1.42857143; - color: #555; -} -.form-control { - display: block; - width: 100%; - height: 34px; - padding: 6px 12px; - font-size: 14px; - line-height: 1.42857143; - color: #555; - background-color: #fff; - background-image: none; - border: 1px solid #ccc; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; - -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; - transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; -} -.form-control:focus { - border-color: #66afe9; - outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); - box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); -} -.form-control::-moz-placeholder { - color: #999; - opacity: 1; -} -.form-control:-ms-input-placeholder { - color: #999; -} -.form-control::-webkit-input-placeholder { - color: #999; -} -.form-control[disabled], -.form-control[readonly], -fieldset[disabled] .form-control { - background-color: #eee; - opacity: 1; -} -.form-control[disabled], -fieldset[disabled] .form-control { - cursor: not-allowed; -} -textarea.form-control { - height: auto; -} -input[type="search"] { - -webkit-appearance: none; -} -@media screen and (-webkit-min-device-pixel-ratio: 0) { - input[type="date"].form-control, - input[type="time"].form-control, - input[type="datetime-local"].form-control, - input[type="month"].form-control { - line-height: 34px; - } - input[type="date"].input-sm, - input[type="time"].input-sm, - input[type="datetime-local"].input-sm, - input[type="month"].input-sm, - .input-group-sm input[type="date"], - .input-group-sm input[type="time"], - .input-group-sm input[type="datetime-local"], - .input-group-sm input[type="month"] { - line-height: 30px; - } - input[type="date"].input-lg, - input[type="time"].input-lg, - input[type="datetime-local"].input-lg, - input[type="month"].input-lg, - .input-group-lg input[type="date"], - .input-group-lg input[type="time"], - .input-group-lg input[type="datetime-local"], - .input-group-lg input[type="month"] { - line-height: 46px; - } -} -.form-group { - margin-bottom: 15px; -} -.radio, -.checkbox { - position: relative; - display: block; - margin-top: 10px; - margin-bottom: 10px; -} -.radio label, -.checkbox label { - min-height: 20px; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - cursor: pointer; -} -.radio input[type="radio"], -.radio-inline input[type="radio"], -.checkbox input[type="checkbox"], -.checkbox-inline input[type="checkbox"] { - position: absolute; - margin-top: 4px \9; - margin-left: -20px; -} -.radio + .radio, -.checkbox + .checkbox { - margin-top: -5px; -} -.radio-inline, -.checkbox-inline { - position: relative; - display: inline-block; - padding-left: 20px; - margin-bottom: 0; - font-weight: normal; - vertical-align: middle; - cursor: pointer; -} -.radio-inline + .radio-inline, -.checkbox-inline + .checkbox-inline { - margin-top: 0; - margin-left: 10px; -} -input[type="radio"][disabled], -input[type="checkbox"][disabled], -input[type="radio"].disabled, -input[type="checkbox"].disabled, -fieldset[disabled] input[type="radio"], -fieldset[disabled] input[type="checkbox"] { - cursor: not-allowed; -} -.radio-inline.disabled, -.checkbox-inline.disabled, -fieldset[disabled] .radio-inline, -fieldset[disabled] .checkbox-inline { - cursor: not-allowed; -} -.radio.disabled label, -.checkbox.disabled label, -fieldset[disabled] .radio label, -fieldset[disabled] .checkbox label { - cursor: not-allowed; -} -.form-control-static { - min-height: 34px; - padding-top: 7px; - padding-bottom: 7px; - margin-bottom: 0; -} -.form-control-static.input-lg, -.form-control-static.input-sm { - padding-right: 0; - padding-left: 0; -} -.input-sm { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -select.input-sm { - height: 30px; - line-height: 30px; -} -textarea.input-sm, -select[multiple].input-sm { - height: auto; -} -.form-group-sm .form-control { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.form-group-sm select.form-control { - height: 30px; - line-height: 30px; -} -.form-group-sm textarea.form-control, -.form-group-sm select[multiple].form-control { - height: auto; -} -.form-group-sm .form-control-static { - height: 30px; - min-height: 32px; - padding: 6px 10px; - font-size: 12px; - line-height: 1.5; -} -.input-lg { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 6px; -} -select.input-lg { - height: 46px; - line-height: 46px; -} -textarea.input-lg, -select[multiple].input-lg { - height: auto; -} -.form-group-lg .form-control { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 6px; -} -.form-group-lg select.form-control { - height: 46px; - line-height: 46px; -} -.form-group-lg textarea.form-control, -.form-group-lg select[multiple].form-control { - height: auto; -} -.form-group-lg .form-control-static { - height: 46px; - min-height: 38px; - padding: 11px 16px; - font-size: 18px; - line-height: 1.3333333; -} -.has-feedback { - position: relative; -} -.has-feedback .form-control { - padding-right: 42.5px; -} -.form-control-feedback { - position: absolute; - top: 0; - right: 0; - z-index: 2; - display: block; - width: 34px; - height: 34px; - line-height: 34px; - text-align: center; - pointer-events: none; -} -.input-lg + .form-control-feedback, -.input-group-lg + .form-control-feedback, -.form-group-lg .form-control + .form-control-feedback { - width: 46px; - height: 46px; - line-height: 46px; -} -.input-sm + .form-control-feedback, -.input-group-sm + .form-control-feedback, -.form-group-sm .form-control + .form-control-feedback { - width: 30px; - height: 30px; - line-height: 30px; -} -.has-success .help-block, -.has-success .control-label, -.has-success .radio, -.has-success .checkbox, -.has-success .radio-inline, -.has-success .checkbox-inline, -.has-success.radio label, -.has-success.checkbox label, -.has-success.radio-inline label, -.has-success.checkbox-inline label { - color: #3c763d; -} -.has-success .form-control { - border-color: #3c763d; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-success .form-control:focus { - border-color: #2b542c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; -} -.has-success .input-group-addon { - color: #3c763d; - background-color: #dff0d8; - border-color: #3c763d; -} -.has-success .form-control-feedback { - color: #3c763d; -} -.has-warning .help-block, -.has-warning .control-label, -.has-warning .radio, -.has-warning .checkbox, -.has-warning .radio-inline, -.has-warning .checkbox-inline, -.has-warning.radio label, -.has-warning.checkbox label, -.has-warning.radio-inline label, -.has-warning.checkbox-inline label { - color: #8a6d3b; -} -.has-warning .form-control { - border-color: #8a6d3b; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-warning .form-control:focus { - border-color: #66512c; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; -} -.has-warning .input-group-addon { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #8a6d3b; -} -.has-warning .form-control-feedback { - color: #8a6d3b; -} -.has-error .help-block, -.has-error .control-label, -.has-error .radio, -.has-error .checkbox, -.has-error .radio-inline, -.has-error .checkbox-inline, -.has-error.radio label, -.has-error.checkbox label, -.has-error.radio-inline label, -.has-error.checkbox-inline label { - color: #a94442; -} -.has-error .form-control { - border-color: #a94442; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); -} -.has-error .form-control:focus { - border-color: #843534; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; -} -.has-error .input-group-addon { - color: #a94442; - background-color: #f2dede; - border-color: #a94442; -} -.has-error .form-control-feedback { - color: #a94442; -} -.has-feedback label ~ .form-control-feedback { - top: 25px; -} -.has-feedback label.sr-only ~ .form-control-feedback { - top: 0; -} -.help-block { - display: block; - margin-top: 5px; - margin-bottom: 10px; - color: #737373; -} -@media (min-width: 768px) { - .form-inline .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .form-inline .form-control-static { - display: inline-block; - } - .form-inline .input-group { - display: inline-table; - vertical-align: middle; - } - .form-inline .input-group .input-group-addon, - .form-inline .input-group .input-group-btn, - .form-inline .input-group .form-control { - width: auto; - } - .form-inline .input-group > .form-control { - width: 100%; - } - .form-inline .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio, - .form-inline .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .form-inline .radio label, - .form-inline .checkbox label { - padding-left: 0; - } - .form-inline .radio input[type="radio"], - .form-inline .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .form-inline .has-feedback .form-control-feedback { - top: 0; - } -} -.form-horizontal .radio, -.form-horizontal .checkbox, -.form-horizontal .radio-inline, -.form-horizontal .checkbox-inline { - padding-top: 7px; - margin-top: 0; - margin-bottom: 0; -} -.form-horizontal .radio, -.form-horizontal .checkbox { - min-height: 27px; -} -.form-horizontal .form-group { - margin-right: -15px; - margin-left: -15px; -} -@media (min-width: 768px) { - .form-horizontal .control-label { - padding-top: 7px; - margin-bottom: 0; - text-align: right; - } -} -.form-horizontal .has-feedback .form-control-feedback { - right: 15px; -} -@media (min-width: 768px) { - .form-horizontal .form-group-lg .control-label { - padding-top: 14.333333px; - font-size: 18px; - } -} -@media (min-width: 768px) { - .form-horizontal .form-group-sm .control-label { - padding-top: 6px; - font-size: 12px; - } -} -.btn { - display: inline-block; - padding: 6px 12px; - margin-bottom: 0; - font-size: 14px; - font-weight: normal; - line-height: 1.42857143; - text-align: center; - white-space: nowrap; - vertical-align: middle; - -ms-touch-action: manipulation; - touch-action: manipulation; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; -} -.btn:focus, -.btn:active:focus, -.btn.active:focus, -.btn.focus, -.btn:active.focus, -.btn.active.focus { - outline: thin dotted; - outline: 5px auto -webkit-focus-ring-color; - outline-offset: -2px; -} -.btn:hover, -.btn:focus, -.btn.focus { - color: #333; - text-decoration: none; -} -.btn:active, -.btn.active { - background-image: none; - outline: 0; - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); -} -.btn.disabled, -.btn[disabled], -fieldset[disabled] .btn { - cursor: not-allowed; - filter: alpha(opacity=65); - -webkit-box-shadow: none; - box-shadow: none; - opacity: .65; -} -a.btn.disabled, -fieldset[disabled] a.btn { - pointer-events: none; -} -.btn-default { - color: #333; - background-color: #fff; - border-color: #ccc; -} -.btn-default:focus, -.btn-default.focus { - color: #333; - background-color: #e6e6e6; - border-color: #8c8c8c; -} -.btn-default:hover { - color: #333; - background-color: #e6e6e6; - border-color: #adadad; -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - color: #333; - background-color: #e6e6e6; - border-color: #adadad; -} -.btn-default:active:hover, -.btn-default.active:hover, -.open > .dropdown-toggle.btn-default:hover, -.btn-default:active:focus, -.btn-default.active:focus, -.open > .dropdown-toggle.btn-default:focus, -.btn-default:active.focus, -.btn-default.active.focus, -.open > .dropdown-toggle.btn-default.focus { - color: #333; - background-color: #d4d4d4; - border-color: #8c8c8c; -} -.btn-default:active, -.btn-default.active, -.open > .dropdown-toggle.btn-default { - background-image: none; -} -.btn-default.disabled, -.btn-default[disabled], -fieldset[disabled] .btn-default, -.btn-default.disabled:hover, -.btn-default[disabled]:hover, -fieldset[disabled] .btn-default:hover, -.btn-default.disabled:focus, -.btn-default[disabled]:focus, -fieldset[disabled] .btn-default:focus, -.btn-default.disabled.focus, -.btn-default[disabled].focus, -fieldset[disabled] .btn-default.focus, -.btn-default.disabled:active, -.btn-default[disabled]:active, -fieldset[disabled] .btn-default:active, -.btn-default.disabled.active, -.btn-default[disabled].active, -fieldset[disabled] .btn-default.active { - background-color: #fff; - border-color: #ccc; -} -.btn-default .badge { - color: #fff; - background-color: #333; -} -.btn-primary { - color: #fff; - background-color: #337ab7; - border-color: #2e6da4; -} -.btn-primary:focus, -.btn-primary.focus { - color: #fff; - background-color: #286090; - border-color: #122b40; -} -.btn-primary:hover { - color: #fff; - background-color: #286090; - border-color: #204d74; -} -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - color: #fff; - background-color: #286090; - border-color: #204d74; -} -.btn-primary:active:hover, -.btn-primary.active:hover, -.open > .dropdown-toggle.btn-primary:hover, -.btn-primary:active:focus, -.btn-primary.active:focus, -.open > .dropdown-toggle.btn-primary:focus, -.btn-primary:active.focus, -.btn-primary.active.focus, -.open > .dropdown-toggle.btn-primary.focus { - color: #fff; - background-color: #204d74; - border-color: #122b40; -} -.btn-primary:active, -.btn-primary.active, -.open > .dropdown-toggle.btn-primary { - background-image: none; -} -.btn-primary.disabled, -.btn-primary[disabled], -fieldset[disabled] .btn-primary, -.btn-primary.disabled:hover, -.btn-primary[disabled]:hover, -fieldset[disabled] .btn-primary:hover, -.btn-primary.disabled:focus, -.btn-primary[disabled]:focus, -fieldset[disabled] .btn-primary:focus, -.btn-primary.disabled.focus, -.btn-primary[disabled].focus, -fieldset[disabled] .btn-primary.focus, -.btn-primary.disabled:active, -.btn-primary[disabled]:active, -fieldset[disabled] .btn-primary:active, -.btn-primary.disabled.active, -.btn-primary[disabled].active, -fieldset[disabled] .btn-primary.active { - background-color: #337ab7; - border-color: #2e6da4; -} -.btn-primary .badge { - color: #337ab7; - background-color: #fff; -} -.btn-success { - color: #fff; - background-color: #5cb85c; - border-color: #4cae4c; -} -.btn-success:focus, -.btn-success.focus { - color: #fff; - background-color: #449d44; - border-color: #255625; -} -.btn-success:hover { - color: #fff; - background-color: #449d44; - border-color: #398439; -} -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - color: #fff; - background-color: #449d44; - border-color: #398439; -} -.btn-success:active:hover, -.btn-success.active:hover, -.open > .dropdown-toggle.btn-success:hover, -.btn-success:active:focus, -.btn-success.active:focus, -.open > .dropdown-toggle.btn-success:focus, -.btn-success:active.focus, -.btn-success.active.focus, -.open > .dropdown-toggle.btn-success.focus { - color: #fff; - background-color: #398439; - border-color: #255625; -} -.btn-success:active, -.btn-success.active, -.open > .dropdown-toggle.btn-success { - background-image: none; -} -.btn-success.disabled, -.btn-success[disabled], -fieldset[disabled] .btn-success, -.btn-success.disabled:hover, -.btn-success[disabled]:hover, -fieldset[disabled] .btn-success:hover, -.btn-success.disabled:focus, -.btn-success[disabled]:focus, -fieldset[disabled] .btn-success:focus, -.btn-success.disabled.focus, -.btn-success[disabled].focus, -fieldset[disabled] .btn-success.focus, -.btn-success.disabled:active, -.btn-success[disabled]:active, -fieldset[disabled] .btn-success:active, -.btn-success.disabled.active, -.btn-success[disabled].active, -fieldset[disabled] .btn-success.active { - background-color: #5cb85c; - border-color: #4cae4c; -} -.btn-success .badge { - color: #5cb85c; - background-color: #fff; -} -.btn-info { - color: #fff; - background-color: #5bc0de; - border-color: #46b8da; -} -.btn-info:focus, -.btn-info.focus { - color: #fff; - background-color: #31b0d5; - border-color: #1b6d85; -} -.btn-info:hover { - color: #fff; - background-color: #31b0d5; - border-color: #269abc; -} -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - color: #fff; - background-color: #31b0d5; - border-color: #269abc; -} -.btn-info:active:hover, -.btn-info.active:hover, -.open > .dropdown-toggle.btn-info:hover, -.btn-info:active:focus, -.btn-info.active:focus, -.open > .dropdown-toggle.btn-info:focus, -.btn-info:active.focus, -.btn-info.active.focus, -.open > .dropdown-toggle.btn-info.focus { - color: #fff; - background-color: #269abc; - border-color: #1b6d85; -} -.btn-info:active, -.btn-info.active, -.open > .dropdown-toggle.btn-info { - background-image: none; -} -.btn-info.disabled, -.btn-info[disabled], -fieldset[disabled] .btn-info, -.btn-info.disabled:hover, -.btn-info[disabled]:hover, -fieldset[disabled] .btn-info:hover, -.btn-info.disabled:focus, -.btn-info[disabled]:focus, -fieldset[disabled] .btn-info:focus, -.btn-info.disabled.focus, -.btn-info[disabled].focus, -fieldset[disabled] .btn-info.focus, -.btn-info.disabled:active, -.btn-info[disabled]:active, -fieldset[disabled] .btn-info:active, -.btn-info.disabled.active, -.btn-info[disabled].active, -fieldset[disabled] .btn-info.active { - background-color: #5bc0de; - border-color: #46b8da; -} -.btn-info .badge { - color: #5bc0de; - background-color: #fff; -} -.btn-warning { - color: #fff; - background-color: #f0ad4e; - border-color: #eea236; -} -.btn-warning:focus, -.btn-warning.focus { - color: #fff; - background-color: #ec971f; - border-color: #985f0d; -} -.btn-warning:hover { - color: #fff; - background-color: #ec971f; - border-color: #d58512; -} -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - color: #fff; - background-color: #ec971f; - border-color: #d58512; -} -.btn-warning:active:hover, -.btn-warning.active:hover, -.open > .dropdown-toggle.btn-warning:hover, -.btn-warning:active:focus, -.btn-warning.active:focus, -.open > .dropdown-toggle.btn-warning:focus, -.btn-warning:active.focus, -.btn-warning.active.focus, -.open > .dropdown-toggle.btn-warning.focus { - color: #fff; - background-color: #d58512; - border-color: #985f0d; -} -.btn-warning:active, -.btn-warning.active, -.open > .dropdown-toggle.btn-warning { - background-image: none; -} -.btn-warning.disabled, -.btn-warning[disabled], -fieldset[disabled] .btn-warning, -.btn-warning.disabled:hover, -.btn-warning[disabled]:hover, -fieldset[disabled] .btn-warning:hover, -.btn-warning.disabled:focus, -.btn-warning[disabled]:focus, -fieldset[disabled] .btn-warning:focus, -.btn-warning.disabled.focus, -.btn-warning[disabled].focus, -fieldset[disabled] .btn-warning.focus, -.btn-warning.disabled:active, -.btn-warning[disabled]:active, -fieldset[disabled] .btn-warning:active, -.btn-warning.disabled.active, -.btn-warning[disabled].active, -fieldset[disabled] .btn-warning.active { - background-color: #f0ad4e; - border-color: #eea236; -} -.btn-warning .badge { - color: #f0ad4e; - background-color: #fff; -} -.btn-danger { - color: #fff; - background-color: #d9534f; - border-color: #d43f3a; -} -.btn-danger:focus, -.btn-danger.focus { - color: #fff; - background-color: #c9302c; - border-color: #761c19; -} -.btn-danger:hover { - color: #fff; - background-color: #c9302c; - border-color: #ac2925; -} -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - color: #fff; - background-color: #c9302c; - border-color: #ac2925; -} -.btn-danger:active:hover, -.btn-danger.active:hover, -.open > .dropdown-toggle.btn-danger:hover, -.btn-danger:active:focus, -.btn-danger.active:focus, -.open > .dropdown-toggle.btn-danger:focus, -.btn-danger:active.focus, -.btn-danger.active.focus, -.open > .dropdown-toggle.btn-danger.focus { - color: #fff; - background-color: #ac2925; - border-color: #761c19; -} -.btn-danger:active, -.btn-danger.active, -.open > .dropdown-toggle.btn-danger { - background-image: none; -} -.btn-danger.disabled, -.btn-danger[disabled], -fieldset[disabled] .btn-danger, -.btn-danger.disabled:hover, -.btn-danger[disabled]:hover, -fieldset[disabled] .btn-danger:hover, -.btn-danger.disabled:focus, -.btn-danger[disabled]:focus, -fieldset[disabled] .btn-danger:focus, -.btn-danger.disabled.focus, -.btn-danger[disabled].focus, -fieldset[disabled] .btn-danger.focus, -.btn-danger.disabled:active, -.btn-danger[disabled]:active, -fieldset[disabled] .btn-danger:active, -.btn-danger.disabled.active, -.btn-danger[disabled].active, -fieldset[disabled] .btn-danger.active { - background-color: #d9534f; - border-color: #d43f3a; -} -.btn-danger .badge { - color: #d9534f; - background-color: #fff; -} -.btn-link { - font-weight: normal; - color: #337ab7; - border-radius: 0; -} -.btn-link, -.btn-link:active, -.btn-link.active, -.btn-link[disabled], -fieldset[disabled] .btn-link { - background-color: transparent; - -webkit-box-shadow: none; - box-shadow: none; -} -.btn-link, -.btn-link:hover, -.btn-link:focus, -.btn-link:active { - border-color: transparent; -} -.btn-link:hover, -.btn-link:focus { - color: #23527c; - text-decoration: underline; - background-color: transparent; -} -.btn-link[disabled]:hover, -fieldset[disabled] .btn-link:hover, -.btn-link[disabled]:focus, -fieldset[disabled] .btn-link:focus { - color: #777; - text-decoration: none; -} -.btn-lg, -.btn-group-lg > .btn { - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 6px; -} -.btn-sm, -.btn-group-sm > .btn { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.btn-xs, -.btn-group-xs > .btn { - padding: 1px 5px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -.btn-block { - display: block; - width: 100%; -} -.btn-block + .btn-block { - margin-top: 5px; -} -input[type="submit"].btn-block, -input[type="reset"].btn-block, -input[type="button"].btn-block { - width: 100%; -} -.fade { - opacity: 0; - -webkit-transition: opacity .15s linear; - -o-transition: opacity .15s linear; - transition: opacity .15s linear; -} -.fade.in { - opacity: 1; -} -.collapse { - display: none; -} -.collapse.in { - display: block; -} -tr.collapse.in { - display: table-row; -} -tbody.collapse.in { - display: table-row-group; -} -.collapsing { - position: relative; - height: 0; - overflow: hidden; - -webkit-transition-timing-function: ease; - -o-transition-timing-function: ease; - transition-timing-function: ease; - -webkit-transition-duration: .35s; - -o-transition-duration: .35s; - transition-duration: .35s; - -webkit-transition-property: height, visibility; - -o-transition-property: height, visibility; - transition-property: height, visibility; -} -.caret { - display: inline-block; - width: 0; - height: 0; - margin-left: 2px; - vertical-align: middle; - border-top: 4px dashed; - border-top: 4px solid \9; - border-right: 4px solid transparent; - border-left: 4px solid transparent; -} -.dropup, -.dropdown { - position: relative; -} -.dropdown-toggle:focus { - outline: 0; -} -.dropdown-menu { - position: absolute; - top: 100%; - left: 0; - z-index: 1000; - display: none; - float: left; - min-width: 160px; - padding: 5px 0; - margin: 2px 0 0; - font-size: 14px; - text-align: left; - list-style: none; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, .15); - border-radius: 4px; - -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); - box-shadow: 0 6px 12px rgba(0, 0, 0, .175); -} -.dropdown-menu.pull-right { - right: 0; - left: auto; -} -.dropdown-menu .divider { - height: 1px; - margin: 9px 0; - overflow: hidden; - background-color: #e5e5e5; -} -.dropdown-menu > li > a { - display: block; - padding: 3px 20px; - clear: both; - font-weight: normal; - line-height: 1.42857143; - color: #333; - white-space: nowrap; -} -.dropdown-menu > li > a:hover, -.dropdown-menu > li > a:focus { - color: #262626; - text-decoration: none; - background-color: #f5f5f5; -} -.dropdown-menu > .active > a, -.dropdown-menu > .active > a:hover, -.dropdown-menu > .active > a:focus { - color: #fff; - text-decoration: none; - background-color: #337ab7; - outline: 0; -} -.dropdown-menu > .disabled > a, -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - color: #777; -} -.dropdown-menu > .disabled > a:hover, -.dropdown-menu > .disabled > a:focus { - text-decoration: none; - cursor: not-allowed; - background-color: transparent; - background-image: none; - filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); -} -.open > .dropdown-menu { - display: block; -} -.open > a { - outline: 0; -} -.dropdown-menu-right { - right: 0; - left: auto; -} -.dropdown-menu-left { - right: auto; - left: 0; -} -.dropdown-header { - display: block; - padding: 3px 20px; - font-size: 12px; - line-height: 1.42857143; - color: #777; - white-space: nowrap; -} -.dropdown-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 990; -} -.pull-right > .dropdown-menu { - right: 0; - left: auto; -} -.dropup .caret, -.navbar-fixed-bottom .dropdown .caret { - content: ""; - border-top: 0; - border-bottom: 4px dashed; - border-bottom: 4px solid \9; -} -.dropup .dropdown-menu, -.navbar-fixed-bottom .dropdown .dropdown-menu { - top: auto; - bottom: 100%; - margin-bottom: 2px; -} -@media (min-width: 768px) { - .navbar-right .dropdown-menu { - right: 0; - left: auto; - } - .navbar-right .dropdown-menu-left { - right: auto; - left: 0; - } -} -.btn-group, -.btn-group-vertical { - position: relative; - display: inline-block; - vertical-align: middle; -} -.btn-group > .btn, -.btn-group-vertical > .btn { - position: relative; - float: left; -} -.btn-group > .btn:hover, -.btn-group-vertical > .btn:hover, -.btn-group > .btn:focus, -.btn-group-vertical > .btn:focus, -.btn-group > .btn:active, -.btn-group-vertical > .btn:active, -.btn-group > .btn.active, -.btn-group-vertical > .btn.active { - z-index: 2; -} -.btn-group .btn + .btn, -.btn-group .btn + .btn-group, -.btn-group .btn-group + .btn, -.btn-group .btn-group + .btn-group { - margin-left: -1px; -} -.btn-toolbar { - margin-left: -5px; -} -.btn-toolbar .btn, -.btn-toolbar .btn-group, -.btn-toolbar .input-group { - float: left; -} -.btn-toolbar > .btn, -.btn-toolbar > .btn-group, -.btn-toolbar > .input-group { - margin-left: 5px; -} -.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { - border-radius: 0; -} -.btn-group > .btn:first-child { - margin-left: 0; -} -.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.btn-group > .btn:last-child:not(:first-child), -.btn-group > .dropdown-toggle:not(:first-child) { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group > .btn-group { - float: left; -} -.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group .dropdown-toggle:active, -.btn-group.open .dropdown-toggle { - outline: 0; -} -.btn-group > .btn + .dropdown-toggle { - padding-right: 8px; - padding-left: 8px; -} -.btn-group > .btn-lg + .dropdown-toggle { - padding-right: 12px; - padding-left: 12px; -} -.btn-group.open .dropdown-toggle { - -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); - box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); -} -.btn-group.open .dropdown-toggle.btn-link { - -webkit-box-shadow: none; - box-shadow: none; -} -.btn .caret { - margin-left: 0; -} -.btn-lg .caret { - border-width: 5px 5px 0; - border-bottom-width: 0; -} -.dropup .btn-lg .caret { - border-width: 0 5px 5px; -} -.btn-group-vertical > .btn, -.btn-group-vertical > .btn-group, -.btn-group-vertical > .btn-group > .btn { - display: block; - float: none; - width: 100%; - max-width: 100%; -} -.btn-group-vertical > .btn-group > .btn { - float: none; -} -.btn-group-vertical > .btn + .btn, -.btn-group-vertical > .btn + .btn-group, -.btn-group-vertical > .btn-group + .btn, -.btn-group-vertical > .btn-group + .btn-group { - margin-top: -1px; - margin-left: 0; -} -.btn-group-vertical > .btn:not(:first-child):not(:last-child) { - border-radius: 0; -} -.btn-group-vertical > .btn:first-child:not(:last-child) { - border-top-right-radius: 4px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn:last-child:not(:first-child) { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: 4px; -} -.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { - border-radius: 0; -} -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, -.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.btn-group-justified { - display: table; - width: 100%; - table-layout: fixed; - border-collapse: separate; -} -.btn-group-justified > .btn, -.btn-group-justified > .btn-group { - display: table-cell; - float: none; - width: 1%; -} -.btn-group-justified > .btn-group .btn { - width: 100%; -} -.btn-group-justified > .btn-group .dropdown-menu { - left: auto; -} -[data-toggle="buttons"] > .btn input[type="radio"], -[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], -[data-toggle="buttons"] > .btn input[type="checkbox"], -[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { - position: absolute; - clip: rect(0, 0, 0, 0); - pointer-events: none; -} -.input-group { - position: relative; - display: table; - border-collapse: separate; -} -.input-group[class*="col-"] { - float: none; - padding-right: 0; - padding-left: 0; -} -.input-group .form-control { - position: relative; - z-index: 2; - float: left; - width: 100%; - margin-bottom: 0; -} -.input-group-lg > .form-control, -.input-group-lg > .input-group-addon, -.input-group-lg > .input-group-btn > .btn { - height: 46px; - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; - border-radius: 6px; -} -select.input-group-lg > .form-control, -select.input-group-lg > .input-group-addon, -select.input-group-lg > .input-group-btn > .btn { - height: 46px; - line-height: 46px; -} -textarea.input-group-lg > .form-control, -textarea.input-group-lg > .input-group-addon, -textarea.input-group-lg > .input-group-btn > .btn, -select[multiple].input-group-lg > .form-control, -select[multiple].input-group-lg > .input-group-addon, -select[multiple].input-group-lg > .input-group-btn > .btn { - height: auto; -} -.input-group-sm > .form-control, -.input-group-sm > .input-group-addon, -.input-group-sm > .input-group-btn > .btn { - height: 30px; - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; - border-radius: 3px; -} -select.input-group-sm > .form-control, -select.input-group-sm > .input-group-addon, -select.input-group-sm > .input-group-btn > .btn { - height: 30px; - line-height: 30px; -} -textarea.input-group-sm > .form-control, -textarea.input-group-sm > .input-group-addon, -textarea.input-group-sm > .input-group-btn > .btn, -select[multiple].input-group-sm > .form-control, -select[multiple].input-group-sm > .input-group-addon, -select[multiple].input-group-sm > .input-group-btn > .btn { - height: auto; -} -.input-group-addon, -.input-group-btn, -.input-group .form-control { - display: table-cell; -} -.input-group-addon:not(:first-child):not(:last-child), -.input-group-btn:not(:first-child):not(:last-child), -.input-group .form-control:not(:first-child):not(:last-child) { - border-radius: 0; -} -.input-group-addon, -.input-group-btn { - width: 1%; - white-space: nowrap; - vertical-align: middle; -} -.input-group-addon { - padding: 6px 12px; - font-size: 14px; - font-weight: normal; - line-height: 1; - color: #555; - text-align: center; - background-color: #eee; - border: 1px solid #ccc; - border-radius: 4px; -} -.input-group-addon.input-sm { - padding: 5px 10px; - font-size: 12px; - border-radius: 3px; -} -.input-group-addon.input-lg { - padding: 10px 16px; - font-size: 18px; - border-radius: 6px; -} -.input-group-addon input[type="radio"], -.input-group-addon input[type="checkbox"] { - margin-top: 0; -} -.input-group .form-control:first-child, -.input-group-addon:first-child, -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group > .btn, -.input-group-btn:first-child > .dropdown-toggle, -.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), -.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { - border-top-right-radius: 0; - border-bottom-right-radius: 0; -} -.input-group-addon:first-child { - border-right: 0; -} -.input-group .form-control:last-child, -.input-group-addon:last-child, -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group > .btn, -.input-group-btn:last-child > .dropdown-toggle, -.input-group-btn:first-child > .btn:not(:first-child), -.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { - border-top-left-radius: 0; - border-bottom-left-radius: 0; -} -.input-group-addon:last-child { - border-left: 0; -} -.input-group-btn { - position: relative; - font-size: 0; - white-space: nowrap; -} -.input-group-btn > .btn { - position: relative; -} -.input-group-btn > .btn + .btn { - margin-left: -1px; -} -.input-group-btn > .btn:hover, -.input-group-btn > .btn:focus, -.input-group-btn > .btn:active { - z-index: 2; -} -.input-group-btn:first-child > .btn, -.input-group-btn:first-child > .btn-group { - margin-right: -1px; -} -.input-group-btn:last-child > .btn, -.input-group-btn:last-child > .btn-group { - z-index: 2; - margin-left: -1px; -} -.nav { - padding-left: 0; - margin-bottom: 0; - list-style: none; -} -.nav > li { - position: relative; - display: block; -} -.nav > li > a { - position: relative; - display: block; - padding: 10px 15px; -} -.nav > li > a:hover, -.nav > li > a:focus { - text-decoration: none; - background-color: #eee; -} -.nav > li.disabled > a { - color: #777; -} -.nav > li.disabled > a:hover, -.nav > li.disabled > a:focus { - color: #777; - text-decoration: none; - cursor: not-allowed; - background-color: transparent; -} -.nav .open > a, -.nav .open > a:hover, -.nav .open > a:focus { - background-color: #eee; - border-color: #337ab7; -} -.nav .nav-divider { - height: 1px; - margin: 9px 0; - overflow: hidden; - background-color: #e5e5e5; -} -.nav > li > a > img { - max-width: none; -} -.nav-tabs { - border-bottom: 1px solid #ddd; -} -.nav-tabs > li { - float: left; - margin-bottom: -1px; -} -.nav-tabs > li > a { - margin-right: 2px; - line-height: 1.42857143; - border: 1px solid transparent; - border-radius: 4px 4px 0 0; -} -.nav-tabs > li > a:hover { - border-color: #eee #eee #ddd; -} -.nav-tabs > li.active > a, -.nav-tabs > li.active > a:hover, -.nav-tabs > li.active > a:focus { - color: #555; - cursor: default; - background-color: #fff; - border: 1px solid #ddd; - border-bottom-color: transparent; -} -.nav-tabs.nav-justified { - width: 100%; - border-bottom: 0; -} -.nav-tabs.nav-justified > li { - float: none; -} -.nav-tabs.nav-justified > li > a { - margin-bottom: 5px; - text-align: center; -} -.nav-tabs.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; -} -@media (min-width: 768px) { - .nav-tabs.nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-tabs.nav-justified > li > a { - margin-bottom: 0; - } -} -.nav-tabs.nav-justified > li > a { - margin-right: 0; - border-radius: 4px; -} -.nav-tabs.nav-justified > .active > a, -.nav-tabs.nav-justified > .active > a:hover, -.nav-tabs.nav-justified > .active > a:focus { - border: 1px solid #ddd; -} -@media (min-width: 768px) { - .nav-tabs.nav-justified > li > a { - border-bottom: 1px solid #ddd; - border-radius: 4px 4px 0 0; - } - .nav-tabs.nav-justified > .active > a, - .nav-tabs.nav-justified > .active > a:hover, - .nav-tabs.nav-justified > .active > a:focus { - border-bottom-color: #fff; - } -} -.nav-pills > li { - float: left; -} -.nav-pills > li > a { - border-radius: 4px; -} -.nav-pills > li + li { - margin-left: 2px; -} -.nav-pills > li.active > a, -.nav-pills > li.active > a:hover, -.nav-pills > li.active > a:focus { - color: #fff; - background-color: #337ab7; -} -.nav-stacked > li { - float: none; -} -.nav-stacked > li + li { - margin-top: 2px; - margin-left: 0; -} -.nav-justified { - width: 100%; -} -.nav-justified > li { - float: none; -} -.nav-justified > li > a { - margin-bottom: 5px; - text-align: center; -} -.nav-justified > .dropdown .dropdown-menu { - top: auto; - left: auto; -} -@media (min-width: 768px) { - .nav-justified > li { - display: table-cell; - width: 1%; - } - .nav-justified > li > a { - margin-bottom: 0; - } -} -.nav-tabs-justified { - border-bottom: 0; -} -.nav-tabs-justified > li > a { - margin-right: 0; - border-radius: 4px; -} -.nav-tabs-justified > .active > a, -.nav-tabs-justified > .active > a:hover, -.nav-tabs-justified > .active > a:focus { - border: 1px solid #ddd; -} -@media (min-width: 768px) { - .nav-tabs-justified > li > a { - border-bottom: 1px solid #ddd; - border-radius: 4px 4px 0 0; - } - .nav-tabs-justified > .active > a, - .nav-tabs-justified > .active > a:hover, - .nav-tabs-justified > .active > a:focus { - border-bottom-color: #fff; - } -} -.tab-content > .tab-pane { - display: none; -} -.tab-content > .active { - display: block; -} -.nav-tabs .dropdown-menu { - margin-top: -1px; - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.navbar { - position: relative; - min-height: 50px; - margin-bottom: 20px; - border: 1px solid transparent; -} -@media (min-width: 768px) { - .navbar { - border-radius: 4px; - } -} -@media (min-width: 768px) { - .navbar-header { - float: left; - } -} -.navbar-collapse { - padding-right: 15px; - padding-left: 15px; - overflow-x: visible; - -webkit-overflow-scrolling: touch; - border-top: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); -} -.navbar-collapse.in { - overflow-y: auto; -} -@media (min-width: 768px) { - .navbar-collapse { - width: auto; - border-top: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-collapse.collapse { - display: block !important; - height: auto !important; - padding-bottom: 0; - overflow: visible !important; - } - .navbar-collapse.in { - overflow-y: visible; - } - .navbar-fixed-top .navbar-collapse, - .navbar-static-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - padding-right: 0; - padding-left: 0; - } -} -.navbar-fixed-top .navbar-collapse, -.navbar-fixed-bottom .navbar-collapse { - max-height: 340px; -} -@media (max-device-width: 480px) and (orientation: landscape) { - .navbar-fixed-top .navbar-collapse, - .navbar-fixed-bottom .navbar-collapse { - max-height: 200px; - } -} -.container > .navbar-header, -.container-fluid > .navbar-header, -.container > .navbar-collapse, -.container-fluid > .navbar-collapse { - margin-right: -15px; - margin-left: -15px; -} -@media (min-width: 768px) { - .container > .navbar-header, - .container-fluid > .navbar-header, - .container > .navbar-collapse, - .container-fluid > .navbar-collapse { - margin-right: 0; - margin-left: 0; - } -} -.navbar-static-top { - z-index: 1000; - border-width: 0 0 1px; -} -@media (min-width: 768px) { - .navbar-static-top { - border-radius: 0; - } -} -.navbar-fixed-top, -.navbar-fixed-bottom { - position: fixed; - right: 0; - left: 0; - z-index: 1030; -} -@media (min-width: 768px) { - .navbar-fixed-top, - .navbar-fixed-bottom { - border-radius: 0; - } -} -.navbar-fixed-top { - top: 0; - border-width: 0 0 1px; -} -.navbar-fixed-bottom { - bottom: 0; - margin-bottom: 0; - border-width: 1px 0 0; -} -.navbar-brand { - float: left; - height: 50px; - padding: 15px 15px; - font-size: 18px; - line-height: 20px; -} -.navbar-brand:hover, -.navbar-brand:focus { - text-decoration: none; -} -.navbar-brand > img { - display: block; -} -@media (min-width: 768px) { - .navbar > .container .navbar-brand, - .navbar > .container-fluid .navbar-brand { - margin-left: -15px; - } -} -.navbar-toggle { - position: relative; - float: right; - padding: 9px 10px; - margin-top: 8px; - margin-right: 15px; - margin-bottom: 8px; - background-color: transparent; - background-image: none; - border: 1px solid transparent; - border-radius: 4px; -} -.navbar-toggle:focus { - outline: 0; -} -.navbar-toggle .icon-bar { - display: block; - width: 22px; - height: 2px; - border-radius: 1px; -} -.navbar-toggle .icon-bar + .icon-bar { - margin-top: 4px; -} -@media (min-width: 768px) { - .navbar-toggle { - display: none; - } -} -.navbar-nav { - margin: 7.5px -15px; -} -.navbar-nav > li > a { - padding-top: 10px; - padding-bottom: 10px; - line-height: 20px; -} -@media (max-width: 767px) { - .navbar-nav .open .dropdown-menu { - position: static; - float: none; - width: auto; - margin-top: 0; - background-color: transparent; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - } - .navbar-nav .open .dropdown-menu > li > a, - .navbar-nav .open .dropdown-menu .dropdown-header { - padding: 5px 15px 5px 25px; - } - .navbar-nav .open .dropdown-menu > li > a { - line-height: 20px; - } - .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-nav .open .dropdown-menu > li > a:focus { - background-image: none; - } -} -@media (min-width: 768px) { - .navbar-nav { - float: left; - margin: 0; - } - .navbar-nav > li { - float: left; - } - .navbar-nav > li > a { - padding-top: 15px; - padding-bottom: 15px; - } -} -.navbar-form { - padding: 10px 15px; - margin-top: 8px; - margin-right: -15px; - margin-bottom: 8px; - margin-left: -15px; - border-top: 1px solid transparent; - border-bottom: 1px solid transparent; - -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); -} -@media (min-width: 768px) { - .navbar-form .form-group { - display: inline-block; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .form-control { - display: inline-block; - width: auto; - vertical-align: middle; - } - .navbar-form .form-control-static { - display: inline-block; - } - .navbar-form .input-group { - display: inline-table; - vertical-align: middle; - } - .navbar-form .input-group .input-group-addon, - .navbar-form .input-group .input-group-btn, - .navbar-form .input-group .form-control { - width: auto; - } - .navbar-form .input-group > .form-control { - width: 100%; - } - .navbar-form .control-label { - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .radio, - .navbar-form .checkbox { - display: inline-block; - margin-top: 0; - margin-bottom: 0; - vertical-align: middle; - } - .navbar-form .radio label, - .navbar-form .checkbox label { - padding-left: 0; - } - .navbar-form .radio input[type="radio"], - .navbar-form .checkbox input[type="checkbox"] { - position: relative; - margin-left: 0; - } - .navbar-form .has-feedback .form-control-feedback { - top: 0; - } -} -@media (max-width: 767px) { - .navbar-form .form-group { - margin-bottom: 5px; - } - .navbar-form .form-group:last-child { - margin-bottom: 0; - } -} -@media (min-width: 768px) { - .navbar-form { - width: auto; - padding-top: 0; - padding-bottom: 0; - margin-right: 0; - margin-left: 0; - border: 0; - -webkit-box-shadow: none; - box-shadow: none; - } -} -.navbar-nav > li > .dropdown-menu { - margin-top: 0; - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { - margin-bottom: 0; - border-top-left-radius: 4px; - border-top-right-radius: 4px; - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; -} -.navbar-btn { - margin-top: 8px; - margin-bottom: 8px; -} -.navbar-btn.btn-sm { - margin-top: 10px; - margin-bottom: 10px; -} -.navbar-btn.btn-xs { - margin-top: 14px; - margin-bottom: 14px; -} -.navbar-text { - margin-top: 15px; - margin-bottom: 15px; -} -@media (min-width: 768px) { - .navbar-text { - float: left; - margin-right: 15px; - margin-left: 15px; - } -} -@media (min-width: 768px) { - .navbar-left { - float: left !important; - } - .navbar-right { - float: right !important; - margin-right: -15px; - } - .navbar-right ~ .navbar-right { - margin-right: 0; - } -} -.navbar-default { - background-color: #f8f8f8; - border-color: #e7e7e7; -} -.navbar-default .navbar-brand { - color: #777; -} -.navbar-default .navbar-brand:hover, -.navbar-default .navbar-brand:focus { - color: #5e5e5e; - background-color: transparent; -} -.navbar-default .navbar-text { - color: #777; -} -.navbar-default .navbar-nav > li > a { - color: #777; -} -.navbar-default .navbar-nav > li > a:hover, -.navbar-default .navbar-nav > li > a:focus { - color: #333; - background-color: transparent; -} -.navbar-default .navbar-nav > .active > a, -.navbar-default .navbar-nav > .active > a:hover, -.navbar-default .navbar-nav > .active > a:focus { - color: #555; - background-color: #e7e7e7; -} -.navbar-default .navbar-nav > .disabled > a, -.navbar-default .navbar-nav > .disabled > a:hover, -.navbar-default .navbar-nav > .disabled > a:focus { - color: #ccc; - background-color: transparent; -} -.navbar-default .navbar-toggle { - border-color: #ddd; -} -.navbar-default .navbar-toggle:hover, -.navbar-default .navbar-toggle:focus { - background-color: #ddd; -} -.navbar-default .navbar-toggle .icon-bar { - background-color: #888; -} -.navbar-default .navbar-collapse, -.navbar-default .navbar-form { - border-color: #e7e7e7; -} -.navbar-default .navbar-nav > .open > a, -.navbar-default .navbar-nav > .open > a:hover, -.navbar-default .navbar-nav > .open > a:focus { - color: #555; - background-color: #e7e7e7; -} -@media (max-width: 767px) { - .navbar-default .navbar-nav .open .dropdown-menu > li > a { - color: #777; - } - .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { - color: #333; - background-color: transparent; - } - .navbar-default .navbar-nav .open .dropdown-menu > .active > a, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #555; - background-color: #e7e7e7; - } - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #ccc; - background-color: transparent; - } -} -.navbar-default .navbar-link { - color: #777; -} -.navbar-default .navbar-link:hover { - color: #333; -} -.navbar-default .btn-link { - color: #777; -} -.navbar-default .btn-link:hover, -.navbar-default .btn-link:focus { - color: #333; -} -.navbar-default .btn-link[disabled]:hover, -fieldset[disabled] .navbar-default .btn-link:hover, -.navbar-default .btn-link[disabled]:focus, -fieldset[disabled] .navbar-default .btn-link:focus { - color: #ccc; -} -.navbar-inverse { - background-color: #222; - border-color: #080808; -} -.navbar-inverse .navbar-brand { - color: #9d9d9d; -} -.navbar-inverse .navbar-brand:hover, -.navbar-inverse .navbar-brand:focus { - color: #fff; - background-color: transparent; -} -.navbar-inverse .navbar-text { - color: #9d9d9d; -} -.navbar-inverse .navbar-nav > li > a { - color: #9d9d9d; -} -.navbar-inverse .navbar-nav > li > a:hover, -.navbar-inverse .navbar-nav > li > a:focus { - color: #fff; - background-color: transparent; -} -.navbar-inverse .navbar-nav > .active > a, -.navbar-inverse .navbar-nav > .active > a:hover, -.navbar-inverse .navbar-nav > .active > a:focus { - color: #fff; - background-color: #080808; -} -.navbar-inverse .navbar-nav > .disabled > a, -.navbar-inverse .navbar-nav > .disabled > a:hover, -.navbar-inverse .navbar-nav > .disabled > a:focus { - color: #444; - background-color: transparent; -} -.navbar-inverse .navbar-toggle { - border-color: #333; -} -.navbar-inverse .navbar-toggle:hover, -.navbar-inverse .navbar-toggle:focus { - background-color: #333; -} -.navbar-inverse .navbar-toggle .icon-bar { - background-color: #fff; -} -.navbar-inverse .navbar-collapse, -.navbar-inverse .navbar-form { - border-color: #101010; -} -.navbar-inverse .navbar-nav > .open > a, -.navbar-inverse .navbar-nav > .open > a:hover, -.navbar-inverse .navbar-nav > .open > a:focus { - color: #fff; - background-color: #080808; -} -@media (max-width: 767px) { - .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { - border-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu .divider { - background-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { - color: #9d9d9d; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { - color: #fff; - background-color: transparent; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { - color: #fff; - background-color: #080808; - } - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, - .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { - color: #444; - background-color: transparent; - } -} -.navbar-inverse .navbar-link { - color: #9d9d9d; -} -.navbar-inverse .navbar-link:hover { - color: #fff; -} -.navbar-inverse .btn-link { - color: #9d9d9d; -} -.navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link:focus { - color: #fff; -} -.navbar-inverse .btn-link[disabled]:hover, -fieldset[disabled] .navbar-inverse .btn-link:hover, -.navbar-inverse .btn-link[disabled]:focus, -fieldset[disabled] .navbar-inverse .btn-link:focus { - color: #444; -} -.breadcrumb { - padding: 8px 15px; - margin-bottom: 20px; - list-style: none; - background-color: #f5f5f5; - border-radius: 4px; -} -.breadcrumb > li { - display: inline-block; -} -.breadcrumb > li + li:before { - padding: 0 5px; - color: #ccc; - content: "/\00a0"; -} -.breadcrumb > .active { - color: #777; -} -.pagination { - display: inline-block; - padding-left: 0; - margin: 20px 0; - border-radius: 4px; -} -.pagination > li { - display: inline; -} -.pagination > li > a, -.pagination > li > span { - position: relative; - float: left; - padding: 6px 12px; - margin-left: -1px; - line-height: 1.42857143; - color: #337ab7; - text-decoration: none; - background-color: #fff; - border: 1px solid #ddd; -} -.pagination > li:first-child > a, -.pagination > li:first-child > span { - margin-left: 0; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} -.pagination > li:last-child > a, -.pagination > li:last-child > span { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} -.pagination > li > a:hover, -.pagination > li > span:hover, -.pagination > li > a:focus, -.pagination > li > span:focus { - z-index: 3; - color: #23527c; - background-color: #eee; - border-color: #ddd; -} -.pagination > .active > a, -.pagination > .active > span, -.pagination > .active > a:hover, -.pagination > .active > span:hover, -.pagination > .active > a:focus, -.pagination > .active > span:focus { - z-index: 2; - color: #fff; - cursor: default; - background-color: #337ab7; - border-color: #337ab7; -} -.pagination > .disabled > span, -.pagination > .disabled > span:hover, -.pagination > .disabled > span:focus, -.pagination > .disabled > a, -.pagination > .disabled > a:hover, -.pagination > .disabled > a:focus { - color: #777; - cursor: not-allowed; - background-color: #fff; - border-color: #ddd; -} -.pagination-lg > li > a, -.pagination-lg > li > span { - padding: 10px 16px; - font-size: 18px; - line-height: 1.3333333; -} -.pagination-lg > li:first-child > a, -.pagination-lg > li:first-child > span { - border-top-left-radius: 6px; - border-bottom-left-radius: 6px; -} -.pagination-lg > li:last-child > a, -.pagination-lg > li:last-child > span { - border-top-right-radius: 6px; - border-bottom-right-radius: 6px; -} -.pagination-sm > li > a, -.pagination-sm > li > span { - padding: 5px 10px; - font-size: 12px; - line-height: 1.5; -} -.pagination-sm > li:first-child > a, -.pagination-sm > li:first-child > span { - border-top-left-radius: 3px; - border-bottom-left-radius: 3px; -} -.pagination-sm > li:last-child > a, -.pagination-sm > li:last-child > span { - border-top-right-radius: 3px; - border-bottom-right-radius: 3px; -} -.pager { - padding-left: 0; - margin: 20px 0; - text-align: center; - list-style: none; -} -.pager li { - display: inline; -} -.pager li > a, -.pager li > span { - display: inline-block; - padding: 5px 14px; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 15px; -} -.pager li > a:hover, -.pager li > a:focus { - text-decoration: none; - background-color: #eee; -} -.pager .next > a, -.pager .next > span { - float: right; -} -.pager .previous > a, -.pager .previous > span { - float: left; -} -.pager .disabled > a, -.pager .disabled > a:hover, -.pager .disabled > a:focus, -.pager .disabled > span { - color: #777; - cursor: not-allowed; - background-color: #fff; -} -.label { - display: inline; - padding: .2em .6em .3em; - font-size: 75%; - font-weight: bold; - line-height: 1; - color: #fff; - text-align: center; - white-space: nowrap; - vertical-align: baseline; - border-radius: .25em; -} -a.label:hover, -a.label:focus { - color: #fff; - text-decoration: none; - cursor: pointer; -} -.label:empty { - display: none; -} -.btn .label { - position: relative; - top: -1px; -} -.label-default { - background-color: #777; -} -.label-default[href]:hover, -.label-default[href]:focus { - background-color: #5e5e5e; -} -.label-primary { - background-color: #337ab7; -} -.label-primary[href]:hover, -.label-primary[href]:focus { - background-color: #286090; -} -.label-success { - background-color: #5cb85c; -} -.label-success[href]:hover, -.label-success[href]:focus { - background-color: #449d44; -} -.label-info { - background-color: #5bc0de; -} -.label-info[href]:hover, -.label-info[href]:focus { - background-color: #31b0d5; -} -.label-warning { - background-color: #f0ad4e; -} -.label-warning[href]:hover, -.label-warning[href]:focus { - background-color: #ec971f; -} -.label-danger { - background-color: #d9534f; -} -.label-danger[href]:hover, -.label-danger[href]:focus { - background-color: #c9302c; -} -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: 12px; - font-weight: bold; - line-height: 1; - color: #fff; - text-align: center; - white-space: nowrap; - vertical-align: middle; - background-color: #777; - border-radius: 10px; -} -.badge:empty { - display: none; -} -.btn .badge { - position: relative; - top: -1px; -} -.btn-xs .badge, -.btn-group-xs > .btn .badge { - top: 0; - padding: 1px 5px; -} -a.badge:hover, -a.badge:focus { - color: #fff; - text-decoration: none; - cursor: pointer; -} -.list-group-item.active > .badge, -.nav-pills > .active > a > .badge { - color: #337ab7; - background-color: #fff; -} -.list-group-item > .badge { - float: right; -} -.list-group-item > .badge + .badge { - margin-right: 5px; -} -.nav-pills > li > a > .badge { - margin-left: 3px; -} -.jumbotron { - padding-top: 30px; - padding-bottom: 30px; - margin-bottom: 30px; - color: inherit; - background-color: #eee; -} -.jumbotron h1, -.jumbotron .h1 { - color: inherit; -} -.jumbotron p { - margin-bottom: 15px; - font-size: 21px; - font-weight: 200; -} -.jumbotron > hr { - border-top-color: #d5d5d5; -} -.container .jumbotron, -.container-fluid .jumbotron { - border-radius: 6px; -} -.jumbotron .container { - max-width: 100%; -} -@media screen and (min-width: 768px) { - .jumbotron { - padding-top: 48px; - padding-bottom: 48px; - } - .container .jumbotron, - .container-fluid .jumbotron { - padding-right: 60px; - padding-left: 60px; - } - .jumbotron h1, - .jumbotron .h1 { - font-size: 63px; - } -} -.thumbnail { - display: block; - padding: 4px; - margin-bottom: 20px; - line-height: 1.42857143; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 4px; - -webkit-transition: border .2s ease-in-out; - -o-transition: border .2s ease-in-out; - transition: border .2s ease-in-out; -} -.thumbnail > img, -.thumbnail a > img { - margin-right: auto; - margin-left: auto; -} -a.thumbnail:hover, -a.thumbnail:focus, -a.thumbnail.active { - border-color: #337ab7; -} -.thumbnail .caption { - padding: 9px; - color: #333; -} -.alert { - padding: 15px; - margin-bottom: 20px; - border: 1px solid transparent; - border-radius: 4px; -} -.alert h4 { - margin-top: 0; - color: inherit; -} -.alert .alert-link { - font-weight: bold; -} -.alert > p, -.alert > ul { - margin-bottom: 0; -} -.alert > p + p { - margin-top: 5px; -} -.alert-dismissable, -.alert-dismissible { - padding-right: 35px; -} -.alert-dismissable .close, -.alert-dismissible .close { - position: relative; - top: -2px; - right: -21px; - color: inherit; -} -.alert-success { - color: #3c763d; - background-color: #dff0d8; - border-color: #d6e9c6; -} -.alert-success hr { - border-top-color: #c9e2b3; -} -.alert-success .alert-link { - color: #2b542c; -} -.alert-info { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; -} -.alert-info hr { - border-top-color: #a6e1ec; -} -.alert-info .alert-link { - color: #245269; -} -.alert-warning { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; -} -.alert-warning hr { - border-top-color: #f7e1b5; -} -.alert-warning .alert-link { - color: #66512c; -} -.alert-danger { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; -} -.alert-danger hr { - border-top-color: #e4b9c0; -} -.alert-danger .alert-link { - color: #843534; -} -@-webkit-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@-o-keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -@keyframes progress-bar-stripes { - from { - background-position: 40px 0; - } - to { - background-position: 0 0; - } -} -.progress { - height: 20px; - margin-bottom: 20px; - overflow: hidden; - background-color: #f5f5f5; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); -} -.progress-bar { - float: left; - width: 0; - height: 100%; - font-size: 12px; - line-height: 20px; - color: #fff; - text-align: center; - background-color: #337ab7; - -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); - box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); - -webkit-transition: width .6s ease; - -o-transition: width .6s ease; - transition: width .6s ease; -} -.progress-striped .progress-bar, -.progress-bar-striped { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - background-size: 40px 40px; -} -.progress.active .progress-bar, -.progress-bar.active { - -webkit-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; -} -.progress-bar-success { - background-color: #5cb85c; -} -.progress-striped .progress-bar-success { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-info { - background-color: #5bc0de; -} -.progress-striped .progress-bar-info { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-warning { - background-color: #f0ad4e; -} -.progress-striped .progress-bar-warning { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.progress-bar-danger { - background-color: #d9534f; -} -.progress-striped .progress-bar-danger { - background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); -} -.media { - margin-top: 15px; -} -.media:first-child { - margin-top: 0; -} -.media, -.media-body { - overflow: hidden; - zoom: 1; -} -.media-body { - width: 10000px; -} -.media-object { - display: block; -} -.media-object.img-thumbnail { - max-width: none; -} -.media-right, -.media > .pull-right { - padding-left: 10px; -} -.media-left, -.media > .pull-left { - padding-right: 10px; -} -.media-left, -.media-right, -.media-body { - display: table-cell; - vertical-align: top; -} -.media-middle { - vertical-align: middle; -} -.media-bottom { - vertical-align: bottom; -} -.media-heading { - margin-top: 0; - margin-bottom: 5px; -} -.media-list { - padding-left: 0; - list-style: none; -} -.list-group { - padding-left: 0; - margin-bottom: 20px; -} -.list-group-item { - position: relative; - display: block; - padding: 10px 15px; - margin-bottom: -1px; - background-color: #fff; - border: 1px solid #ddd; -} -.list-group-item:first-child { - border-top-left-radius: 4px; - border-top-right-radius: 4px; -} -.list-group-item:last-child { - margin-bottom: 0; - border-bottom-right-radius: 4px; - border-bottom-left-radius: 4px; -} -a.list-group-item, -button.list-group-item { - color: #555; -} -a.list-group-item .list-group-item-heading, -button.list-group-item .list-group-item-heading { - color: #333; -} -a.list-group-item:hover, -button.list-group-item:hover, -a.list-group-item:focus, -button.list-group-item:focus { - color: #555; - text-decoration: none; - background-color: #f5f5f5; -} -button.list-group-item { - width: 100%; - text-align: left; -} -.list-group-item.disabled, -.list-group-item.disabled:hover, -.list-group-item.disabled:focus { - color: #777; - cursor: not-allowed; - background-color: #eee; -} -.list-group-item.disabled .list-group-item-heading, -.list-group-item.disabled:hover .list-group-item-heading, -.list-group-item.disabled:focus .list-group-item-heading { - color: inherit; -} -.list-group-item.disabled .list-group-item-text, -.list-group-item.disabled:hover .list-group-item-text, -.list-group-item.disabled:focus .list-group-item-text { - color: #777; -} -.list-group-item.active, -.list-group-item.active:hover, -.list-group-item.active:focus { - z-index: 2; - color: #fff; - background-color: #337ab7; - border-color: #337ab7; -} -.list-group-item.active .list-group-item-heading, -.list-group-item.active:hover .list-group-item-heading, -.list-group-item.active:focus .list-group-item-heading, -.list-group-item.active .list-group-item-heading > small, -.list-group-item.active:hover .list-group-item-heading > small, -.list-group-item.active:focus .list-group-item-heading > small, -.list-group-item.active .list-group-item-heading > .small, -.list-group-item.active:hover .list-group-item-heading > .small, -.list-group-item.active:focus .list-group-item-heading > .small { - color: inherit; -} -.list-group-item.active .list-group-item-text, -.list-group-item.active:hover .list-group-item-text, -.list-group-item.active:focus .list-group-item-text { - color: #c7ddef; -} -.list-group-item-success { - color: #3c763d; - background-color: #dff0d8; -} -a.list-group-item-success, -button.list-group-item-success { - color: #3c763d; -} -a.list-group-item-success .list-group-item-heading, -button.list-group-item-success .list-group-item-heading { - color: inherit; -} -a.list-group-item-success:hover, -button.list-group-item-success:hover, -a.list-group-item-success:focus, -button.list-group-item-success:focus { - color: #3c763d; - background-color: #d0e9c6; -} -a.list-group-item-success.active, -button.list-group-item-success.active, -a.list-group-item-success.active:hover, -button.list-group-item-success.active:hover, -a.list-group-item-success.active:focus, -button.list-group-item-success.active:focus { - color: #fff; - background-color: #3c763d; - border-color: #3c763d; -} -.list-group-item-info { - color: #31708f; - background-color: #d9edf7; -} -a.list-group-item-info, -button.list-group-item-info { - color: #31708f; -} -a.list-group-item-info .list-group-item-heading, -button.list-group-item-info .list-group-item-heading { - color: inherit; -} -a.list-group-item-info:hover, -button.list-group-item-info:hover, -a.list-group-item-info:focus, -button.list-group-item-info:focus { - color: #31708f; - background-color: #c4e3f3; -} -a.list-group-item-info.active, -button.list-group-item-info.active, -a.list-group-item-info.active:hover, -button.list-group-item-info.active:hover, -a.list-group-item-info.active:focus, -button.list-group-item-info.active:focus { - color: #fff; - background-color: #31708f; - border-color: #31708f; -} -.list-group-item-warning { - color: #8a6d3b; - background-color: #fcf8e3; -} -a.list-group-item-warning, -button.list-group-item-warning { - color: #8a6d3b; -} -a.list-group-item-warning .list-group-item-heading, -button.list-group-item-warning .list-group-item-heading { - color: inherit; -} -a.list-group-item-warning:hover, -button.list-group-item-warning:hover, -a.list-group-item-warning:focus, -button.list-group-item-warning:focus { - color: #8a6d3b; - background-color: #faf2cc; -} -a.list-group-item-warning.active, -button.list-group-item-warning.active, -a.list-group-item-warning.active:hover, -button.list-group-item-warning.active:hover, -a.list-group-item-warning.active:focus, -button.list-group-item-warning.active:focus { - color: #fff; - background-color: #8a6d3b; - border-color: #8a6d3b; -} -.list-group-item-danger { - color: #a94442; - background-color: #f2dede; -} -a.list-group-item-danger, -button.list-group-item-danger { - color: #a94442; -} -a.list-group-item-danger .list-group-item-heading, -button.list-group-item-danger .list-group-item-heading { - color: inherit; -} -a.list-group-item-danger:hover, -button.list-group-item-danger:hover, -a.list-group-item-danger:focus, -button.list-group-item-danger:focus { - color: #a94442; - background-color: #ebcccc; -} -a.list-group-item-danger.active, -button.list-group-item-danger.active, -a.list-group-item-danger.active:hover, -button.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus, -button.list-group-item-danger.active:focus { - color: #fff; - background-color: #a94442; - border-color: #a94442; -} -.list-group-item-heading { - margin-top: 0; - margin-bottom: 5px; -} -.list-group-item-text { - margin-bottom: 0; - line-height: 1.3; -} -.panel { - margin-bottom: 20px; - background-color: #fff; - border: 1px solid transparent; - border-radius: 4px; - -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); - box-shadow: 0 1px 1px rgba(0, 0, 0, .05); -} -.panel-body { - padding: 15px; -} -.panel-heading { - padding: 10px 15px; - border-bottom: 1px solid transparent; - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel-heading > .dropdown .dropdown-toggle { - color: inherit; -} -.panel-title { - margin-top: 0; - margin-bottom: 0; - font-size: 16px; - color: inherit; -} -.panel-title > a, -.panel-title > small, -.panel-title > .small, -.panel-title > small > a, -.panel-title > .small > a { - color: inherit; -} -.panel-footer { - padding: 10px 15px; - background-color: #f5f5f5; - border-top: 1px solid #ddd; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel > .list-group, -.panel > .panel-collapse > .list-group { - margin-bottom: 0; -} -.panel > .list-group .list-group-item, -.panel > .panel-collapse > .list-group .list-group-item { - border-width: 1px 0; - border-radius: 0; -} -.panel > .list-group:first-child .list-group-item:first-child, -.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { - border-top: 0; - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel > .list-group:last-child .list-group-item:last-child, -.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { - border-bottom: 0; - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child { - border-top-left-radius: 0; - border-top-right-radius: 0; -} -.panel-heading + .list-group .list-group-item:first-child { - border-top-width: 0; -} -.list-group + .panel-footer { - border-top-width: 0; -} -.panel > .table, -.panel > .table-responsive > .table, -.panel > .panel-collapse > .table { - margin-bottom: 0; -} -.panel > .table caption, -.panel > .table-responsive > .table caption, -.panel > .panel-collapse > .table caption { - padding-right: 15px; - padding-left: 15px; -} -.panel > .table:first-child, -.panel > .table-responsive:first-child > .table:first-child { - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel > .table:first-child > thead:first-child > tr:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { - border-top-left-radius: 3px; - border-top-right-radius: 3px; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { - border-top-left-radius: 3px; -} -.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, -.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, -.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, -.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { - border-top-right-radius: 3px; -} -.panel > .table:last-child, -.panel > .table-responsive:last-child > .table:last-child { - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { - border-bottom-right-radius: 3px; - border-bottom-left-radius: 3px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { - border-bottom-left-radius: 3px; -} -.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, -.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, -.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, -.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { - border-bottom-right-radius: 3px; -} -.panel > .panel-body + .table, -.panel > .panel-body + .table-responsive, -.panel > .table + .panel-body, -.panel > .table-responsive + .panel-body { - border-top: 1px solid #ddd; -} -.panel > .table > tbody:first-child > tr:first-child th, -.panel > .table > tbody:first-child > tr:first-child td { - border-top: 0; -} -.panel > .table-bordered, -.panel > .table-responsive > .table-bordered { - border: 0; -} -.panel > .table-bordered > thead > tr > th:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, -.panel > .table-bordered > tbody > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, -.panel > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, -.panel > .table-bordered > thead > tr > td:first-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, -.panel > .table-bordered > tbody > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, -.panel > .table-bordered > tfoot > tr > td:first-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { - border-left: 0; -} -.panel > .table-bordered > thead > tr > th:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, -.panel > .table-bordered > tbody > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, -.panel > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, -.panel > .table-bordered > thead > tr > td:last-child, -.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, -.panel > .table-bordered > tbody > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, -.panel > .table-bordered > tfoot > tr > td:last-child, -.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { - border-right: 0; -} -.panel > .table-bordered > thead > tr:first-child > td, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, -.panel > .table-bordered > tbody > tr:first-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, -.panel > .table-bordered > thead > tr:first-child > th, -.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, -.panel > .table-bordered > tbody > tr:first-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { - border-bottom: 0; -} -.panel > .table-bordered > tbody > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, -.panel > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, -.panel > .table-bordered > tbody > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, -.panel > .table-bordered > tfoot > tr:last-child > th, -.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { - border-bottom: 0; -} -.panel > .table-responsive { - margin-bottom: 0; - border: 0; -} -.panel-group { - margin-bottom: 20px; -} -.panel-group .panel { - margin-bottom: 0; - border-radius: 4px; -} -.panel-group .panel + .panel { - margin-top: 5px; -} -.panel-group .panel-heading { - border-bottom: 0; -} -.panel-group .panel-heading + .panel-collapse > .panel-body, -.panel-group .panel-heading + .panel-collapse > .list-group { - border-top: 1px solid #ddd; -} -.panel-group .panel-footer { - border-top: 0; -} -.panel-group .panel-footer + .panel-collapse .panel-body { - border-bottom: 1px solid #ddd; -} -.panel-default { - border-color: #ddd; -} -.panel-default > .panel-heading { - color: #333; - background-color: #f5f5f5; - border-color: #ddd; -} -.panel-default > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #ddd; -} -.panel-default > .panel-heading .badge { - color: #f5f5f5; - background-color: #333; -} -.panel-default > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #ddd; -} -.panel-primary { - border-color: #337ab7; -} -.panel-primary > .panel-heading { - color: #fff; - background-color: #337ab7; - border-color: #337ab7; -} -.panel-primary > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #337ab7; -} -.panel-primary > .panel-heading .badge { - color: #337ab7; - background-color: #fff; -} -.panel-primary > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #337ab7; -} -.panel-success { - border-color: #d6e9c6; -} -.panel-success > .panel-heading { - color: #3c763d; - background-color: #dff0d8; - border-color: #d6e9c6; -} -.panel-success > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #d6e9c6; -} -.panel-success > .panel-heading .badge { - color: #dff0d8; - background-color: #3c763d; -} -.panel-success > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #d6e9c6; -} -.panel-info { - border-color: #bce8f1; -} -.panel-info > .panel-heading { - color: #31708f; - background-color: #d9edf7; - border-color: #bce8f1; -} -.panel-info > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #bce8f1; -} -.panel-info > .panel-heading .badge { - color: #d9edf7; - background-color: #31708f; -} -.panel-info > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #bce8f1; -} -.panel-warning { - border-color: #faebcc; -} -.panel-warning > .panel-heading { - color: #8a6d3b; - background-color: #fcf8e3; - border-color: #faebcc; -} -.panel-warning > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #faebcc; -} -.panel-warning > .panel-heading .badge { - color: #fcf8e3; - background-color: #8a6d3b; -} -.panel-warning > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #faebcc; -} -.panel-danger { - border-color: #ebccd1; -} -.panel-danger > .panel-heading { - color: #a94442; - background-color: #f2dede; - border-color: #ebccd1; -} -.panel-danger > .panel-heading + .panel-collapse > .panel-body { - border-top-color: #ebccd1; -} -.panel-danger > .panel-heading .badge { - color: #f2dede; - background-color: #a94442; -} -.panel-danger > .panel-footer + .panel-collapse > .panel-body { - border-bottom-color: #ebccd1; -} -.embed-responsive { - position: relative; - display: block; - height: 0; - padding: 0; - overflow: hidden; -} -.embed-responsive .embed-responsive-item, -.embed-responsive iframe, -.embed-responsive embed, -.embed-responsive object, -.embed-responsive video { - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 100%; - height: 100%; - border: 0; -} -.embed-responsive-16by9 { - padding-bottom: 56.25%; -} -.embed-responsive-4by3 { - padding-bottom: 75%; -} -.well { - min-height: 20px; - padding: 19px; - margin-bottom: 20px; - background-color: #f5f5f5; - border: 1px solid #e3e3e3; - border-radius: 4px; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); -} -.well blockquote { - border-color: #ddd; - border-color: rgba(0, 0, 0, .15); -} -.well-lg { - padding: 24px; - border-radius: 6px; -} -.well-sm { - padding: 9px; - border-radius: 3px; -} -.close { - float: right; - font-size: 21px; - font-weight: bold; - line-height: 1; - color: #000; - text-shadow: 0 1px 0 #fff; - filter: alpha(opacity=20); - opacity: .2; -} -.close:hover, -.close:focus { - color: #000; - text-decoration: none; - cursor: pointer; - filter: alpha(opacity=50); - opacity: .5; -} -button.close { - -webkit-appearance: none; - padding: 0; - cursor: pointer; - background: transparent; - border: 0; -} -.modal-open { - overflow: hidden; -} -.modal { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1050; - display: none; - overflow: hidden; - -webkit-overflow-scrolling: touch; - outline: 0; -} -.modal.fade .modal-dialog { - -webkit-transition: -webkit-transform .3s ease-out; - -o-transition: -o-transform .3s ease-out; - transition: transform .3s ease-out; - -webkit-transform: translate(0, -25%); - -ms-transform: translate(0, -25%); - -o-transform: translate(0, -25%); - transform: translate(0, -25%); -} -.modal.in .modal-dialog { - -webkit-transform: translate(0, 0); - -ms-transform: translate(0, 0); - -o-transform: translate(0, 0); - transform: translate(0, 0); -} -.modal-open .modal { - overflow-x: hidden; - overflow-y: auto; -} -.modal-dialog { - position: relative; - width: auto; - margin: 10px; -} -.modal-content { - position: relative; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #999; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; - outline: 0; - -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); - box-shadow: 0 3px 9px rgba(0, 0, 0, .5); -} -.modal-backdrop { - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - z-index: 1040; - background-color: #000; -} -.modal-backdrop.fade { - filter: alpha(opacity=0); - opacity: 0; -} -.modal-backdrop.in { - filter: alpha(opacity=50); - opacity: .5; -} -.modal-header { - min-height: 16.42857143px; - padding: 15px; - border-bottom: 1px solid #e5e5e5; -} -.modal-header .close { - margin-top: -2px; -} -.modal-title { - margin: 0; - line-height: 1.42857143; -} -.modal-body { - position: relative; - padding: 15px; -} -.modal-footer { - padding: 15px; - text-align: right; - border-top: 1px solid #e5e5e5; -} -.modal-footer .btn + .btn { - margin-bottom: 0; - margin-left: 5px; -} -.modal-footer .btn-group .btn + .btn { - margin-left: -1px; -} -.modal-footer .btn-block + .btn-block { - margin-left: 0; -} -.modal-scrollbar-measure { - position: absolute; - top: -9999px; - width: 50px; - height: 50px; - overflow: scroll; -} -@media (min-width: 768px) { - .modal-dialog { - width: 600px; - margin: 30px auto; - } - .modal-content { - -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); - box-shadow: 0 5px 15px rgba(0, 0, 0, .5); - } - .modal-sm { - width: 300px; - } -} -@media (min-width: 992px) { - .modal-lg { - width: 900px; - } -} -.tooltip { - position: absolute; - z-index: 1070; - display: block; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 12px; - font-style: normal; - font-weight: normal; - line-height: 1.42857143; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - letter-spacing: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; - white-space: normal; - filter: alpha(opacity=0); - opacity: 0; - - line-break: auto; -} -.tooltip.in { - filter: alpha(opacity=90); - opacity: .9; -} -.tooltip.top { - padding: 5px 0; - margin-top: -3px; -} -.tooltip.right { - padding: 0 5px; - margin-left: 3px; -} -.tooltip.bottom { - padding: 5px 0; - margin-top: 3px; -} -.tooltip.left { - padding: 0 5px; - margin-left: -3px; -} -.tooltip-inner { - max-width: 200px; - padding: 3px 8px; - color: #fff; - text-align: center; - background-color: #000; - border-radius: 4px; -} -.tooltip-arrow { - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.tooltip.top .tooltip-arrow { - bottom: 0; - left: 50%; - margin-left: -5px; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.top-left .tooltip-arrow { - right: 5px; - bottom: 0; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.top-right .tooltip-arrow { - bottom: 0; - left: 5px; - margin-bottom: -5px; - border-width: 5px 5px 0; - border-top-color: #000; -} -.tooltip.right .tooltip-arrow { - top: 50%; - left: 0; - margin-top: -5px; - border-width: 5px 5px 5px 0; - border-right-color: #000; -} -.tooltip.left .tooltip-arrow { - top: 50%; - right: 0; - margin-top: -5px; - border-width: 5px 0 5px 5px; - border-left-color: #000; -} -.tooltip.bottom .tooltip-arrow { - top: 0; - left: 50%; - margin-left: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000; -} -.tooltip.bottom-left .tooltip-arrow { - top: 0; - right: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000; -} -.tooltip.bottom-right .tooltip-arrow { - top: 0; - left: 5px; - margin-top: -5px; - border-width: 0 5px 5px; - border-bottom-color: #000; -} -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1060; - display: none; - max-width: 276px; - padding: 1px; - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 14px; - font-style: normal; - font-weight: normal; - line-height: 1.42857143; - text-align: left; - text-align: start; - text-decoration: none; - text-shadow: none; - text-transform: none; - letter-spacing: normal; - word-break: normal; - word-spacing: normal; - word-wrap: normal; - white-space: normal; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); - box-shadow: 0 5px 10px rgba(0, 0, 0, .2); - - line-break: auto; -} -.popover.top { - margin-top: -10px; -} -.popover.right { - margin-left: 10px; -} -.popover.bottom { - margin-top: 10px; -} -.popover.left { - margin-left: -10px; -} -.popover-title { - padding: 8px 14px; - margin: 0; - font-size: 14px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - border-radius: 5px 5px 0 0; -} -.popover-content { - padding: 9px 14px; -} -.popover > .arrow, -.popover > .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.popover > .arrow { - border-width: 11px; -} -.popover > .arrow:after { - content: ""; - border-width: 10px; -} -.popover.top > .arrow { - bottom: -11px; - left: 50%; - margin-left: -11px; - border-top-color: #999; - border-top-color: rgba(0, 0, 0, .25); - border-bottom-width: 0; -} -.popover.top > .arrow:after { - bottom: 1px; - margin-left: -10px; - content: " "; - border-top-color: #fff; - border-bottom-width: 0; -} -.popover.right > .arrow { - top: 50%; - left: -11px; - margin-top: -11px; - border-right-color: #999; - border-right-color: rgba(0, 0, 0, .25); - border-left-width: 0; -} -.popover.right > .arrow:after { - bottom: -10px; - left: 1px; - content: " "; - border-right-color: #fff; - border-left-width: 0; -} -.popover.bottom > .arrow { - top: -11px; - left: 50%; - margin-left: -11px; - border-top-width: 0; - border-bottom-color: #999; - border-bottom-color: rgba(0, 0, 0, .25); -} -.popover.bottom > .arrow:after { - top: 1px; - margin-left: -10px; - content: " "; - border-top-width: 0; - border-bottom-color: #fff; -} -.popover.left > .arrow { - top: 50%; - right: -11px; - margin-top: -11px; - border-right-width: 0; - border-left-color: #999; - border-left-color: rgba(0, 0, 0, .25); -} -.popover.left > .arrow:after { - right: 1px; - bottom: -10px; - content: " "; - border-right-width: 0; - border-left-color: #fff; -} -.carousel { - position: relative; -} -.carousel-inner { - position: relative; - width: 100%; - overflow: hidden; -} -.carousel-inner > .item { - position: relative; - display: none; - -webkit-transition: .6s ease-in-out left; - -o-transition: .6s ease-in-out left; - transition: .6s ease-in-out left; -} -.carousel-inner > .item > img, -.carousel-inner > .item > a > img { - line-height: 1; -} -@media all and (transform-3d), (-webkit-transform-3d) { - .carousel-inner > .item { - -webkit-transition: -webkit-transform .6s ease-in-out; - -o-transition: -o-transform .6s ease-in-out; - transition: transform .6s ease-in-out; - - -webkit-backface-visibility: hidden; - backface-visibility: hidden; - -webkit-perspective: 1000px; - perspective: 1000px; - } - .carousel-inner > .item.next, - .carousel-inner > .item.active.right { - left: 0; - -webkit-transform: translate3d(100%, 0, 0); - transform: translate3d(100%, 0, 0); - } - .carousel-inner > .item.prev, - .carousel-inner > .item.active.left { - left: 0; - -webkit-transform: translate3d(-100%, 0, 0); - transform: translate3d(-100%, 0, 0); - } - .carousel-inner > .item.next.left, - .carousel-inner > .item.prev.right, - .carousel-inner > .item.active { - left: 0; - -webkit-transform: translate3d(0, 0, 0); - transform: translate3d(0, 0, 0); - } -} -.carousel-inner > .active, -.carousel-inner > .next, -.carousel-inner > .prev { - display: block; -} -.carousel-inner > .active { - left: 0; -} -.carousel-inner > .next, -.carousel-inner > .prev { - position: absolute; - top: 0; - width: 100%; -} -.carousel-inner > .next { - left: 100%; -} -.carousel-inner > .prev { - left: -100%; -} -.carousel-inner > .next.left, -.carousel-inner > .prev.right { - left: 0; -} -.carousel-inner > .active.left { - left: -100%; -} -.carousel-inner > .active.right { - left: 100%; -} -.carousel-control { - position: absolute; - top: 0; - bottom: 0; - left: 0; - width: 15%; - font-size: 20px; - color: #fff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, .6); - filter: alpha(opacity=50); - opacity: .5; -} -.carousel-control.left { - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); - background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); - background-repeat: repeat-x; -} -.carousel-control.right { - right: 0; - left: auto; - background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); - background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); - background-repeat: repeat-x; -} -.carousel-control:hover, -.carousel-control:focus { - color: #fff; - text-decoration: none; - filter: alpha(opacity=90); - outline: 0; - opacity: .9; -} -.carousel-control .icon-prev, -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-left, -.carousel-control .glyphicon-chevron-right { - position: absolute; - top: 50%; - z-index: 5; - display: inline-block; - margin-top: -10px; -} -.carousel-control .icon-prev, -.carousel-control .glyphicon-chevron-left { - left: 50%; - margin-left: -10px; -} -.carousel-control .icon-next, -.carousel-control .glyphicon-chevron-right { - right: 50%; - margin-right: -10px; -} -.carousel-control .icon-prev, -.carousel-control .icon-next { - width: 20px; - height: 20px; - font-family: serif; - line-height: 1; -} -.carousel-control .icon-prev:before { - content: '\2039'; -} -.carousel-control .icon-next:before { - content: '\203a'; -} -.carousel-indicators { - position: absolute; - bottom: 10px; - left: 50%; - z-index: 15; - width: 60%; - padding-left: 0; - margin-left: -30%; - text-align: center; - list-style: none; -} -.carousel-indicators li { - display: inline-block; - width: 10px; - height: 10px; - margin: 1px; - text-indent: -999px; - cursor: pointer; - background-color: #000 \9; - background-color: rgba(0, 0, 0, 0); - border: 1px solid #fff; - border-radius: 10px; -} -.carousel-indicators .active { - width: 12px; - height: 12px; - margin: 0; - background-color: #fff; -} -.carousel-caption { - position: absolute; - right: 15%; - bottom: 20px; - left: 15%; - z-index: 10; - padding-top: 20px; - padding-bottom: 20px; - color: #fff; - text-align: center; - text-shadow: 0 1px 2px rgba(0, 0, 0, .6); -} -.carousel-caption .btn { - text-shadow: none; -} -@media screen and (min-width: 768px) { - .carousel-control .glyphicon-chevron-left, - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-prev, - .carousel-control .icon-next { - width: 30px; - height: 30px; - margin-top: -15px; - font-size: 30px; - } - .carousel-control .glyphicon-chevron-left, - .carousel-control .icon-prev { - margin-left: -15px; - } - .carousel-control .glyphicon-chevron-right, - .carousel-control .icon-next { - margin-right: -15px; - } - .carousel-caption { - right: 20%; - left: 20%; - padding-bottom: 30px; - } - .carousel-indicators { - bottom: 20px; - } -} -.clearfix:before, -.clearfix:after, -.dl-horizontal dd:before, -.dl-horizontal dd:after, -.container:before, -.container:after, -.container-fluid:before, -.container-fluid:after, -.row:before, -.row:after, -.form-horizontal .form-group:before, -.form-horizontal .form-group:after, -.btn-toolbar:before, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:before, -.btn-group-vertical > .btn-group:after, -.nav:before, -.nav:after, -.navbar:before, -.navbar:after, -.navbar-header:before, -.navbar-header:after, -.navbar-collapse:before, -.navbar-collapse:after, -.pager:before, -.pager:after, -.panel-body:before, -.panel-body:after, -.modal-footer:before, -.modal-footer:after { - display: table; - content: " "; -} -.clearfix:after, -.dl-horizontal dd:after, -.container:after, -.container-fluid:after, -.row:after, -.form-horizontal .form-group:after, -.btn-toolbar:after, -.btn-group-vertical > .btn-group:after, -.nav:after, -.navbar:after, -.navbar-header:after, -.navbar-collapse:after, -.pager:after, -.panel-body:after, -.modal-footer:after { - clear: both; -} -.center-block { - display: block; - margin-right: auto; - margin-left: auto; -} -.pull-right { - float: right !important; -} -.pull-left { - float: left !important; -} -.hide { - display: none !important; -} -.show { - display: block !important; -} -.invisible { - visibility: hidden; -} -.text-hide { - font: 0/0 a; - color: transparent; - text-shadow: none; - background-color: transparent; - border: 0; -} -.hidden { - display: none !important; -} -.affix { - position: fixed; -} -@-ms-viewport { - width: device-width; -} -.visible-xs, -.visible-sm, -.visible-md, -.visible-lg { - display: none !important; -} -.visible-xs-block, -.visible-xs-inline, -.visible-xs-inline-block, -.visible-sm-block, -.visible-sm-inline, -.visible-sm-inline-block, -.visible-md-block, -.visible-md-inline, -.visible-md-inline-block, -.visible-lg-block, -.visible-lg-inline, -.visible-lg-inline-block { - display: none !important; -} -@media (max-width: 767px) { - .visible-xs { - display: block !important; - } - table.visible-xs { - display: table !important; - } - tr.visible-xs { - display: table-row !important; - } - th.visible-xs, - td.visible-xs { - display: table-cell !important; - } -} -@media (max-width: 767px) { - .visible-xs-block { - display: block !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline { - display: inline !important; - } -} -@media (max-width: 767px) { - .visible-xs-inline-block { - display: inline-block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm { - display: block !important; - } - table.visible-sm { - display: table !important; - } - tr.visible-sm { - display: table-row !important; - } - th.visible-sm, - td.visible-sm { - display: table-cell !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-block { - display: block !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline { - display: inline !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .visible-sm-inline-block { - display: inline-block !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md { - display: block !important; - } - table.visible-md { - display: table !important; - } - tr.visible-md { - display: table-row !important; - } - th.visible-md, - td.visible-md { - display: table-cell !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-block { - display: block !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline { - display: inline !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .visible-md-inline-block { - display: inline-block !important; - } -} -@media (min-width: 1200px) { - .visible-lg { - display: block !important; - } - table.visible-lg { - display: table !important; - } - tr.visible-lg { - display: table-row !important; - } - th.visible-lg, - td.visible-lg { - display: table-cell !important; - } -} -@media (min-width: 1200px) { - .visible-lg-block { - display: block !important; - } -} -@media (min-width: 1200px) { - .visible-lg-inline { - display: inline !important; - } -} -@media (min-width: 1200px) { - .visible-lg-inline-block { - display: inline-block !important; - } -} -@media (max-width: 767px) { - .hidden-xs { - display: none !important; - } -} -@media (min-width: 768px) and (max-width: 991px) { - .hidden-sm { - display: none !important; - } -} -@media (min-width: 992px) and (max-width: 1199px) { - .hidden-md { - display: none !important; - } -} -@media (min-width: 1200px) { - .hidden-lg { - display: none !important; - } -} -.visible-print { - display: none !important; -} -@media print { - .visible-print { - display: block !important; - } - table.visible-print { - display: table !important; - } - tr.visible-print { - display: table-row !important; - } - th.visible-print, - td.visible-print { - display: table-cell !important; - } -} -.visible-print-block { - display: none !important; -} -@media print { - .visible-print-block { - display: block !important; - } -} -.visible-print-inline { - display: none !important; -} -@media print { - .visible-print-inline { - display: inline !important; - } -} -.visible-print-inline-block { - display: none !important; -} -@media print { - .visible-print-inline-block { - display: inline-block !important; - } -} -@media print { - .hidden-print { - display: none !important; - } -} -/*# sourceMappingURL=bootstrap.css.map */ diff --git a/src/Server/OneTrueError.Web/Content/bootstrap.css.map b/src/Server/OneTrueError.Web/Content/bootstrap.css.map deleted file mode 100644 index 9f60ed2b..00000000 --- a/src/Server/OneTrueError.Web/Content/bootstrap.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/mixins/reset-text.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,4EAA4E;ACG5E;EACE,wBAAA;EACA,2BAAA;EACA,+BAAA;CDDD;ACQD;EACE,UAAA;CDND;ACmBD;;;;;;;;;;;;;EAaE,eAAA;CDjBD;ACyBD;;;;EAIE,sBAAA;EACA,yBAAA;CDvBD;AC+BD;EACE,cAAA;EACA,UAAA;CD7BD;ACqCD;;EAEE,cAAA;CDnCD;AC6CD;EACE,8BAAA;CD3CD;ACmDD;;EAEE,WAAA;CDjDD;AC2DD;EACE,0BAAA;CDzDD;ACgED;;EAEE,kBAAA;CD9DD;ACqED;EACE,mBAAA;CDnED;AC2ED;EACE,eAAA;EACA,iBAAA;CDzED;ACgFD;EACE,iBAAA;EACA,YAAA;CD9ED;ACqFD;EACE,eAAA;CDnFD;AC0FD;;EAEE,eAAA;EACA,eAAA;EACA,mBAAA;EACA,yBAAA;CDxFD;AC2FD;EACE,YAAA;CDzFD;AC4FD;EACE,gBAAA;CD1FD;ACoGD;EACE,UAAA;CDlGD;ACyGD;EACE,iBAAA;CDvGD;ACiHD;EACE,iBAAA;CD/GD;ACsHD;EACE,gCAAA;KAAA,6BAAA;UAAA,wBAAA;EACA,UAAA;CDpHD;AC2HD;EACE,eAAA;CDzHD;ACgID;;;;EAIE,kCAAA;EACA,eAAA;CD9HD;ACgJD;;;;;EAKE,eAAA;EACA,cAAA;EACA,UAAA;CD9ID;ACqJD;EACE,kBAAA;CDnJD;AC6JD;;EAEE,qBAAA;CD3JD;ACsKD;;;;EAIE,2BAAA;EACA,gBAAA;CDpKD;AC2KD;;EAEE,gBAAA;CDzKD;ACgLD;;EAEE,UAAA;EACA,WAAA;CD9KD;ACsLD;EACE,oBAAA;CDpLD;AC+LD;;EAEE,+BAAA;KAAA,4BAAA;UAAA,uBAAA;EACA,WAAA;CD7LD;ACsMD;;EAEE,aAAA;CDpMD;AC4MD;EACE,8BAAA;EACA,gCAAA;KAAA,6BAAA;UAAA,wBAAA;CD1MD;ACmND;;EAEE,yBAAA;CDjND;ACwND;EACE,0BAAA;EACA,cAAA;EACA,+BAAA;CDtND;AC8ND;EACE,UAAA;EACA,WAAA;CD5ND;ACmOD;EACE,eAAA;CDjOD;ACyOD;EACE,kBAAA;CDvOD;ACiPD;EACE,0BAAA;EACA,kBAAA;CD/OD;ACkPD;;EAEE,WAAA;CDhPD;AACD,qFAAqF;AElFrF;EA7FI;;;IAGI,mCAAA;IACA,uBAAA;IACA,oCAAA;YAAA,4BAAA;IACA,6BAAA;GFkLL;EE/KC;;IAEI,2BAAA;GFiLL;EE9KC;IACI,6BAAA;GFgLL;EE7KC;IACI,8BAAA;GF+KL;EE1KC;;IAEI,YAAA;GF4KL;EEzKC;;IAEI,uBAAA;IACA,yBAAA;GF2KL;EExKC;IACI,4BAAA;GF0KL;EEvKC;;IAEI,yBAAA;GFyKL;EEtKC;IACI,2BAAA;GFwKL;EErKC;;;IAGI,WAAA;IACA,UAAA;GFuKL;EEpKC;;IAEI,wBAAA;GFsKL;EEhKC;IACI,cAAA;GFkKL;EEhKC;;IAGQ,kCAAA;GFiKT;EE9JC;IACI,uBAAA;GFgKL;EE7JC;IACI,qCAAA;GF+JL;EEhKC;;IAKQ,kCAAA;GF+JT;EE5JC;;IAGQ,kCAAA;GF6JT;CACF;AGnPD;EACE,oCAAA;EACA,sDAAA;EACA,gYAAA;CHqPD;AG7OD;EACE,mBAAA;EACA,SAAA;EACA,sBAAA;EACA,oCAAA;EACA,mBAAA;EACA,oBAAA;EACA,eAAA;EACA,oCAAA;EACA,mCAAA;CH+OD;AG3OmC;EAAW,eAAA;CH8O9C;AG7OmC;EAAW,eAAA;CHgP9C;AG9OmC;;EAAW,iBAAA;CHkP9C;AGjPmC;EAAW,iBAAA;CHoP9C;AGnPmC;EAAW,iBAAA;CHsP9C;AGrPmC;EAAW,iBAAA;CHwP9C;AGvPmC;EAAW,iBAAA;CH0P9C;AGzPmC;EAAW,iBAAA;CH4P9C;AG3PmC;EAAW,iBAAA;CH8P9C;AG7PmC;EAAW,iBAAA;CHgQ9C;AG/PmC;EAAW,iBAAA;CHkQ9C;AGjQmC;EAAW,iBAAA;CHoQ9C;AGnQmC;EAAW,iBAAA;CHsQ9C;AGrQmC;EAAW,iBAAA;CHwQ9C;AGvQmC;EAAW,iBAAA;CH0Q9C;AGzQmC;EAAW,iBAAA;CH4Q9C;AG3QmC;EAAW,iBAAA;CH8Q9C;AG7QmC;EAAW,iBAAA;CHgR9C;AG/QmC;EAAW,iBAAA;CHkR9C;AGjRmC;EAAW,iBAAA;CHoR9C;AGnRmC;EAAW,iBAAA;CHsR9C;AGrRmC;EAAW,iBAAA;CHwR9C;AGvRmC;EAAW,iBAAA;CH0R9C;AGzRmC;EAAW,iBAAA;CH4R9C;AG3RmC;EAAW,iBAAA;CH8R9C;AG7RmC;EAAW,iBAAA;CHgS9C;AG/RmC;EAAW,iBAAA;CHkS9C;AGjSmC;EAAW,iBAAA;CHoS9C;AGnSmC;EAAW,iBAAA;CHsS9C;AGrSmC;EAAW,iBAAA;CHwS9C;AGvSmC;EAAW,iBAAA;CH0S9C;AGzSmC;EAAW,iBAAA;CH4S9C;AG3SmC;EAAW,iBAAA;CH8S9C;AG7SmC;EAAW,iBAAA;CHgT9C;AG/SmC;EAAW,iBAAA;CHkT9C;AGjTmC;EAAW,iBAAA;CHoT9C;AGnTmC;EAAW,iBAAA;CHsT9C;AGrTmC;EAAW,iBAAA;CHwT9C;AGvTmC;EAAW,iBAAA;CH0T9C;AGzTmC;EAAW,iBAAA;CH4T9C;AG3TmC;EAAW,iBAAA;CH8T9C;AG7TmC;EAAW,iBAAA;CHgU9C;AG/TmC;EAAW,iBAAA;CHkU9C;AGjUmC;EAAW,iBAAA;CHoU9C;AGnUmC;EAAW,iBAAA;CHsU9C;AGrUmC;EAAW,iBAAA;CHwU9C;AGvUmC;EAAW,iBAAA;CH0U9C;AGzUmC;EAAW,iBAAA;CH4U9C;AG3UmC;EAAW,iBAAA;CH8U9C;AG7UmC;EAAW,iBAAA;CHgV9C;AG/UmC;EAAW,iBAAA;CHkV9C;AGjVmC;EAAW,iBAAA;CHoV9C;AGnVmC;EAAW,iBAAA;CHsV9C;AGrVmC;EAAW,iBAAA;CHwV9C;AGvVmC;EAAW,iBAAA;CH0V9C;AGzVmC;EAAW,iBAAA;CH4V9C;AG3VmC;EAAW,iBAAA;CH8V9C;AG7VmC;EAAW,iBAAA;CHgW9C;AG/VmC;EAAW,iBAAA;CHkW9C;AGjWmC;EAAW,iBAAA;CHoW9C;AGnWmC;EAAW,iBAAA;CHsW9C;AGrWmC;EAAW,iBAAA;CHwW9C;AGvWmC;EAAW,iBAAA;CH0W9C;AGzWmC;EAAW,iBAAA;CH4W9C;AG3WmC;EAAW,iBAAA;CH8W9C;AG7WmC;EAAW,iBAAA;CHgX9C;AG/WmC;EAAW,iBAAA;CHkX9C;AGjXmC;EAAW,iBAAA;CHoX9C;AGnXmC;EAAW,iBAAA;CHsX9C;AGrXmC;EAAW,iBAAA;CHwX9C;AGvXmC;EAAW,iBAAA;CH0X9C;AGzXmC;EAAW,iBAAA;CH4X9C;AG3XmC;EAAW,iBAAA;CH8X9C;AG7XmC;EAAW,iBAAA;CHgY9C;AG/XmC;EAAW,iBAAA;CHkY9C;AGjYmC;EAAW,iBAAA;CHoY9C;AGnYmC;EAAW,iBAAA;CHsY9C;AGrYmC;EAAW,iBAAA;CHwY9C;AGvYmC;EAAW,iBAAA;CH0Y9C;AGzYmC;EAAW,iBAAA;CH4Y9C;AG3YmC;EAAW,iBAAA;CH8Y9C;AG7YmC;EAAW,iBAAA;CHgZ9C;AG/YmC;EAAW,iBAAA;CHkZ9C;AGjZmC;EAAW,iBAAA;CHoZ9C;AGnZmC;EAAW,iBAAA;CHsZ9C;AGrZmC;EAAW,iBAAA;CHwZ9C;AGvZmC;EAAW,iBAAA;CH0Z9C;AGzZmC;EAAW,iBAAA;CH4Z9C;AG3ZmC;EAAW,iBAAA;CH8Z9C;AG7ZmC;EAAW,iBAAA;CHga9C;AG/ZmC;EAAW,iBAAA;CHka9C;AGjamC;EAAW,iBAAA;CHoa9C;AGnamC;EAAW,iBAAA;CHsa9C;AGramC;EAAW,iBAAA;CHwa9C;AGvamC;EAAW,iBAAA;CH0a9C;AGzamC;EAAW,iBAAA;CH4a9C;AG3amC;EAAW,iBAAA;CH8a9C;AG7amC;EAAW,iBAAA;CHgb9C;AG/amC;EAAW,iBAAA;CHkb9C;AGjbmC;EAAW,iBAAA;CHob9C;AGnbmC;EAAW,iBAAA;CHsb9C;AGrbmC;EAAW,iBAAA;CHwb9C;AGvbmC;EAAW,iBAAA;CH0b9C;AGzbmC;EAAW,iBAAA;CH4b9C;AG3bmC;EAAW,iBAAA;CH8b9C;AG7bmC;EAAW,iBAAA;CHgc9C;AG/bmC;EAAW,iBAAA;CHkc9C;AGjcmC;EAAW,iBAAA;CHoc9C;AGncmC;EAAW,iBAAA;CHsc9C;AGrcmC;EAAW,iBAAA;CHwc9C;AGvcmC;EAAW,iBAAA;CH0c9C;AGzcmC;EAAW,iBAAA;CH4c9C;AG3cmC;EAAW,iBAAA;CH8c9C;AG7cmC;EAAW,iBAAA;CHgd9C;AG/cmC;EAAW,iBAAA;CHkd9C;AGjdmC;EAAW,iBAAA;CHod9C;AGndmC;EAAW,iBAAA;CHsd9C;AGrdmC;EAAW,iBAAA;CHwd9C;AGvdmC;EAAW,iBAAA;CH0d9C;AGzdmC;EAAW,iBAAA;CH4d9C;AG3dmC;EAAW,iBAAA;CH8d9C;AG7dmC;EAAW,iBAAA;CHge9C;AG/dmC;EAAW,iBAAA;CHke9C;AGjemC;EAAW,iBAAA;CHoe9C;AGnemC;EAAW,iBAAA;CHse9C;AGremC;EAAW,iBAAA;CHwe9C;AGvemC;EAAW,iBAAA;CH0e9C;AGzemC;EAAW,iBAAA;CH4e9C;AG3emC;EAAW,iBAAA;CH8e9C;AG7emC;EAAW,iBAAA;CHgf9C;AG/emC;EAAW,iBAAA;CHkf9C;AGjfmC;EAAW,iBAAA;CHof9C;AGnfmC;EAAW,iBAAA;CHsf9C;AGrfmC;EAAW,iBAAA;CHwf9C;AGvfmC;EAAW,iBAAA;CH0f9C;AGzfmC;EAAW,iBAAA;CH4f9C;AG3fmC;EAAW,iBAAA;CH8f9C;AG7fmC;EAAW,iBAAA;CHggB9C;AG/fmC;EAAW,iBAAA;CHkgB9C;AGjgBmC;EAAW,iBAAA;CHogB9C;AGngBmC;EAAW,iBAAA;CHsgB9C;AGrgBmC;EAAW,iBAAA;CHwgB9C;AGvgBmC;EAAW,iBAAA;CH0gB9C;AGzgBmC;EAAW,iBAAA;CH4gB9C;AG3gBmC;EAAW,iBAAA;CH8gB9C;AG7gBmC;EAAW,iBAAA;CHghB9C;AG/gBmC;EAAW,iBAAA;CHkhB9C;AGjhBmC;EAAW,iBAAA;CHohB9C;AGnhBmC;EAAW,iBAAA;CHshB9C;AGrhBmC;EAAW,iBAAA;CHwhB9C;AGvhBmC;EAAW,iBAAA;CH0hB9C;AGzhBmC;EAAW,iBAAA;CH4hB9C;AG3hBmC;EAAW,iBAAA;CH8hB9C;AG7hBmC;EAAW,iBAAA;CHgiB9C;AG/hBmC;EAAW,iBAAA;CHkiB9C;AGjiBmC;EAAW,iBAAA;CHoiB9C;AGniBmC;EAAW,iBAAA;CHsiB9C;AGriBmC;EAAW,iBAAA;CHwiB9C;AGviBmC;EAAW,iBAAA;CH0iB9C;AGziBmC;EAAW,iBAAA;CH4iB9C;AG3iBmC;EAAW,iBAAA;CH8iB9C;AG7iBmC;EAAW,iBAAA;CHgjB9C;AG/iBmC;EAAW,iBAAA;CHkjB9C;AGjjBmC;EAAW,iBAAA;CHojB9C;AGnjBmC;EAAW,iBAAA;CHsjB9C;AGrjBmC;EAAW,iBAAA;CHwjB9C;AGvjBmC;EAAW,iBAAA;CH0jB9C;AGzjBmC;EAAW,iBAAA;CH4jB9C;AG3jBmC;EAAW,iBAAA;CH8jB9C;AG7jBmC;EAAW,iBAAA;CHgkB9C;AG/jBmC;EAAW,iBAAA;CHkkB9C;AGjkBmC;EAAW,iBAAA;CHokB9C;AGnkBmC;EAAW,iBAAA;CHskB9C;AGrkBmC;EAAW,iBAAA;CHwkB9C;AGvkBmC;EAAW,iBAAA;CH0kB9C;AGzkBmC;EAAW,iBAAA;CH4kB9C;AG3kBmC;EAAW,iBAAA;CH8kB9C;AG7kBmC;EAAW,iBAAA;CHglB9C;AG/kBmC;EAAW,iBAAA;CHklB9C;AGjlBmC;EAAW,iBAAA;CHolB9C;AGnlBmC;EAAW,iBAAA;CHslB9C;AGrlBmC;EAAW,iBAAA;CHwlB9C;AGvlBmC;EAAW,iBAAA;CH0lB9C;AGzlBmC;EAAW,iBAAA;CH4lB9C;AG3lBmC;EAAW,iBAAA;CH8lB9C;AG7lBmC;EAAW,iBAAA;CHgmB9C;AG/lBmC;EAAW,iBAAA;CHkmB9C;AGjmBmC;EAAW,iBAAA;CHomB9C;AGnmBmC;EAAW,iBAAA;CHsmB9C;AGrmBmC;EAAW,iBAAA;CHwmB9C;AGvmBmC;EAAW,iBAAA;CH0mB9C;AGzmBmC;EAAW,iBAAA;CH4mB9C;AG3mBmC;EAAW,iBAAA;CH8mB9C;AG7mBmC;EAAW,iBAAA;CHgnB9C;AG/mBmC;EAAW,iBAAA;CHknB9C;AGjnBmC;EAAW,iBAAA;CHonB9C;AGnnBmC;EAAW,iBAAA;CHsnB9C;AGrnBmC;EAAW,iBAAA;CHwnB9C;AGvnBmC;EAAW,iBAAA;CH0nB9C;AGznBmC;EAAW,iBAAA;CH4nB9C;AG3nBmC;EAAW,iBAAA;CH8nB9C;AG7nBmC;EAAW,iBAAA;CHgoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AG/nBmC;EAAW,iBAAA;CHkoB9C;AGjoBmC;EAAW,iBAAA;CHooB9C;AGnoBmC;EAAW,iBAAA;CHsoB9C;AGroBmC;EAAW,iBAAA;CHwoB9C;AGvoBmC;EAAW,iBAAA;CH0oB9C;AGzoBmC;EAAW,iBAAA;CH4oB9C;AG3oBmC;EAAW,iBAAA;CH8oB9C;AG7oBmC;EAAW,iBAAA;CHgpB9C;AG/oBmC;EAAW,iBAAA;CHkpB9C;AGjpBmC;EAAW,iBAAA;CHopB9C;AGnpBmC;EAAW,iBAAA;CHspB9C;AGrpBmC;EAAW,iBAAA;CHwpB9C;AGvpBmC;EAAW,iBAAA;CH0pB9C;AGzpBmC;EAAW,iBAAA;CH4pB9C;AG3pBmC;EAAW,iBAAA;CH8pB9C;AG7pBmC;EAAW,iBAAA;CHgqB9C;AG/pBmC;EAAW,iBAAA;CHkqB9C;AGjqBmC;EAAW,iBAAA;CHoqB9C;AGnqBmC;EAAW,iBAAA;CHsqB9C;AGrqBmC;EAAW,iBAAA;CHwqB9C;AGvqBmC;EAAW,iBAAA;CH0qB9C;AGzqBmC;EAAW,iBAAA;CH4qB9C;AG3qBmC;EAAW,iBAAA;CH8qB9C;AG7qBmC;EAAW,iBAAA;CHgrB9C;AG/qBmC;EAAW,iBAAA;CHkrB9C;AGjrBmC;EAAW,iBAAA;CHorB9C;AGnrBmC;EAAW,iBAAA;CHsrB9C;AGrrBmC;EAAW,iBAAA;CHwrB9C;AGvrBmC;EAAW,iBAAA;CH0rB9C;AGzrBmC;EAAW,iBAAA;CH4rB9C;AG3rBmC;EAAW,iBAAA;CH8rB9C;AG7rBmC;EAAW,iBAAA;CHgsB9C;AG/rBmC;EAAW,iBAAA;CHksB9C;AGjsBmC;EAAW,iBAAA;CHosB9C;AGnsBmC;EAAW,iBAAA;CHssB9C;AGrsBmC;EAAW,iBAAA;CHwsB9C;AGvsBmC;EAAW,iBAAA;CH0sB9C;AGzsBmC;EAAW,iBAAA;CH4sB9C;AG3sBmC;EAAW,iBAAA;CH8sB9C;AG7sBmC;EAAW,iBAAA;CHgtB9C;AG/sBmC;EAAW,iBAAA;CHktB9C;AGjtBmC;EAAW,iBAAA;CHotB9C;AGntBmC;EAAW,iBAAA;CHstB9C;AGrtBmC;EAAW,iBAAA;CHwtB9C;AGvtBmC;EAAW,iBAAA;CH0tB9C;AGztBmC;EAAW,iBAAA;CH4tB9C;AG3tBmC;EAAW,iBAAA;CH8tB9C;AG7tBmC;EAAW,iBAAA;CHguB9C;AG/tBmC;EAAW,iBAAA;CHkuB9C;AGjuBmC;EAAW,iBAAA;CHouB9C;AGnuBmC;EAAW,iBAAA;CHsuB9C;AGruBmC;EAAW,iBAAA;CHwuB9C;AGvuBmC;EAAW,iBAAA;CH0uB9C;AGzuBmC;EAAW,iBAAA;CH4uB9C;AG3uBmC;EAAW,iBAAA;CH8uB9C;AG7uBmC;EAAW,iBAAA;CHgvB9C;AIthCD;ECgEE,+BAAA;EACG,4BAAA;EACK,uBAAA;CLy9BT;AIxhCD;;EC6DE,+BAAA;EACG,4BAAA;EACK,uBAAA;CL+9BT;AIthCD;EACE,gBAAA;EACA,8CAAA;CJwhCD;AIrhCD;EACE,4DAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,0BAAA;CJuhCD;AInhCD;;;;EAIE,qBAAA;EACA,mBAAA;EACA,qBAAA;CJqhCD;AI/gCD;EACE,eAAA;EACA,sBAAA;CJihCD;AI/gCC;;EAEE,eAAA;EACA,2BAAA;CJihCH;AI9gCC;EErDA,qBAAA;EAEA,2CAAA;EACA,qBAAA;CNqkCD;AIxgCD;EACE,UAAA;CJ0gCD;AIpgCD;EACE,uBAAA;CJsgCD;AIlgCD;;;;;EGvEE,eAAA;EACA,gBAAA;EACA,aAAA;CPglCD;AItgCD;EACE,mBAAA;CJwgCD;AIlgCD;EACE,aAAA;EACA,wBAAA;EACA,0BAAA;EACA,0BAAA;EACA,mBAAA;EC6FA,yCAAA;EACK,oCAAA;EACG,iCAAA;EEvLR,sBAAA;EACA,gBAAA;EACA,aAAA;CPgmCD;AIlgCD;EACE,mBAAA;CJogCD;AI9/BD;EACE,iBAAA;EACA,oBAAA;EACA,UAAA;EACA,8BAAA;CJggCD;AIx/BD;EACE,mBAAA;EACA,WAAA;EACA,YAAA;EACA,aAAA;EACA,WAAA;EACA,iBAAA;EACA,uBAAA;EACA,UAAA;CJ0/BD;AIl/BC;;EAEE,iBAAA;EACA,YAAA;EACA,aAAA;EACA,UAAA;EACA,kBAAA;EACA,WAAA;CJo/BH;AIz+BD;EACE,gBAAA;CJ2+BD;AQloCD;;;;;;;;;;;;EAEE,qBAAA;EACA,iBAAA;EACA,iBAAA;EACA,eAAA;CR8oCD;AQnpCD;;;;;;;;;;;;;;;;;;;;;;;;EASI,oBAAA;EACA,eAAA;EACA,eAAA;CRoqCH;AQhqCD;;;;;;EAGE,iBAAA;EACA,oBAAA;CRqqCD;AQzqCD;;;;;;;;;;;;EAQI,eAAA;CR+qCH;AQ5qCD;;;;;;EAGE,iBAAA;EACA,oBAAA;CRirCD;AQrrCD;;;;;;;;;;;;EAQI,eAAA;CR2rCH;AQvrCD;;EAAU,gBAAA;CR2rCT;AQ1rCD;;EAAU,gBAAA;CR8rCT;AQ7rCD;;EAAU,gBAAA;CRisCT;AQhsCD;;EAAU,gBAAA;CRosCT;AQnsCD;;EAAU,gBAAA;CRusCT;AQtsCD;;EAAU,gBAAA;CR0sCT;AQpsCD;EACE,iBAAA;CRssCD;AQnsCD;EACE,oBAAA;EACA,gBAAA;EACA,iBAAA;EACA,iBAAA;CRqsCD;AQhsCD;EAAA;IAFI,gBAAA;GRssCD;CACF;AQ9rCD;;EAEE,eAAA;CRgsCD;AQ7rCD;;EAEE,0BAAA;EACA,cAAA;CR+rCD;AQ3rCD;EAAuB,iBAAA;CR8rCtB;AQ7rCD;EAAuB,kBAAA;CRgsCtB;AQ/rCD;EAAuB,mBAAA;CRksCtB;AQjsCD;EAAuB,oBAAA;CRosCtB;AQnsCD;EAAuB,oBAAA;CRssCtB;AQnsCD;EAAuB,0BAAA;CRssCtB;AQrsCD;EAAuB,0BAAA;CRwsCtB;AQvsCD;EAAuB,2BAAA;CR0sCtB;AQvsCD;EACE,eAAA;CRysCD;AQvsCD;ECrGE,eAAA;CT+yCD;AS9yCC;;EAEE,eAAA;CTgzCH;AQ3sCD;ECxGE,eAAA;CTszCD;ASrzCC;;EAEE,eAAA;CTuzCH;AQ/sCD;EC3GE,eAAA;CT6zCD;AS5zCC;;EAEE,eAAA;CT8zCH;AQntCD;EC9GE,eAAA;CTo0CD;ASn0CC;;EAEE,eAAA;CTq0CH;AQvtCD;ECjHE,eAAA;CT20CD;AS10CC;;EAEE,eAAA;CT40CH;AQvtCD;EAGE,YAAA;EE3HA,0BAAA;CVm1CD;AUl1CC;;EAEE,0BAAA;CVo1CH;AQztCD;EE9HE,0BAAA;CV01CD;AUz1CC;;EAEE,0BAAA;CV21CH;AQ7tCD;EEjIE,0BAAA;CVi2CD;AUh2CC;;EAEE,0BAAA;CVk2CH;AQjuCD;EEpIE,0BAAA;CVw2CD;AUv2CC;;EAEE,0BAAA;CVy2CH;AQruCD;EEvIE,0BAAA;CV+2CD;AU92CC;;EAEE,0BAAA;CVg3CH;AQpuCD;EACE,oBAAA;EACA,oBAAA;EACA,iCAAA;CRsuCD;AQ9tCD;;EAEE,cAAA;EACA,oBAAA;CRguCD;AQnuCD;;;;EAMI,iBAAA;CRmuCH;AQ5tCD;EACE,gBAAA;EACA,iBAAA;CR8tCD;AQ1tCD;EALE,gBAAA;EACA,iBAAA;EAMA,kBAAA;CR6tCD;AQ/tCD;EAKI,sBAAA;EACA,kBAAA;EACA,mBAAA;CR6tCH;AQxtCD;EACE,cAAA;EACA,oBAAA;CR0tCD;AQxtCD;;EAEE,wBAAA;CR0tCD;AQxtCD;EACE,kBAAA;CR0tCD;AQxtCD;EACE,eAAA;CR0tCD;AQjsCD;EAAA;IAVM,YAAA;IACA,aAAA;IACA,YAAA;IACA,kBAAA;IGtNJ,iBAAA;IACA,wBAAA;IACA,oBAAA;GXs6CC;EQ3sCH;IAHM,mBAAA;GRitCH;CACF;AQxsCD;;EAGE,aAAA;EACA,kCAAA;CRysCD;AQvsCD;EACE,eAAA;EA9IqB,0BAAA;CRw1CtB;AQrsCD;EACE,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,+BAAA;CRusCD;AQlsCG;;;EACE,iBAAA;CRssCL;AQhtCD;;;EAmBI,eAAA;EACA,eAAA;EACA,wBAAA;EACA,eAAA;CRksCH;AQhsCG;;;EACE,uBAAA;CRosCL;AQ5rCD;;EAEE,oBAAA;EACA,gBAAA;EACA,gCAAA;EACA,eAAA;EACA,kBAAA;CR8rCD;AQxrCG;;;;;;EAAW,YAAA;CRgsCd;AQ/rCG;;;;;;EACE,uBAAA;CRssCL;AQhsCD;EACE,oBAAA;EACA,mBAAA;EACA,wBAAA;CRksCD;AYx+CD;;;;EAIE,+DAAA;CZ0+CD;AYt+CD;EACE,iBAAA;EACA,eAAA;EACA,eAAA;EACA,0BAAA;EACA,mBAAA;CZw+CD;AYp+CD;EACE,iBAAA;EACA,eAAA;EACA,eAAA;EACA,0BAAA;EACA,mBAAA;EACA,uDAAA;UAAA,+CAAA;CZs+CD;AY5+CD;EASI,WAAA;EACA,gBAAA;EACA,kBAAA;EACA,yBAAA;UAAA,iBAAA;CZs+CH;AYj+CD;EACE,eAAA;EACA,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,sBAAA;EACA,sBAAA;EACA,eAAA;EACA,0BAAA;EACA,0BAAA;EACA,mBAAA;CZm+CD;AY9+CD;EAeI,WAAA;EACA,mBAAA;EACA,eAAA;EACA,sBAAA;EACA,8BAAA;EACA,iBAAA;CZk+CH;AY79CD;EACE,kBAAA;EACA,mBAAA;CZ+9CD;AazhDD;ECHE,mBAAA;EACA,kBAAA;EACA,mBAAA;EACA,oBAAA;Cd+hDD;AazhDC;EAAA;IAFE,aAAA;Gb+hDD;CACF;Aa3hDC;EAAA;IAFE,aAAA;GbiiDD;CACF;Aa7hDD;EAAA;IAFI,cAAA;GbmiDD;CACF;Aa1hDD;ECvBE,mBAAA;EACA,kBAAA;EACA,mBAAA;EACA,oBAAA;CdojDD;AavhDD;ECvBE,mBAAA;EACA,oBAAA;CdijDD;AejjDG;EACE,mBAAA;EAEA,gBAAA;EAEA,mBAAA;EACA,oBAAA;CfijDL;AejiDG;EACE,YAAA;CfmiDL;Ae5hDC;EACE,YAAA;Cf8hDH;Ae/hDC;EACE,oBAAA;CfiiDH;AeliDC;EACE,oBAAA;CfoiDH;AeriDC;EACE,WAAA;CfuiDH;AexiDC;EACE,oBAAA;Cf0iDH;Ae3iDC;EACE,oBAAA;Cf6iDH;Ae9iDC;EACE,WAAA;CfgjDH;AejjDC;EACE,oBAAA;CfmjDH;AepjDC;EACE,oBAAA;CfsjDH;AevjDC;EACE,WAAA;CfyjDH;Ae1jDC;EACE,oBAAA;Cf4jDH;Ae7jDC;EACE,mBAAA;Cf+jDH;AejjDC;EACE,YAAA;CfmjDH;AepjDC;EACE,oBAAA;CfsjDH;AevjDC;EACE,oBAAA;CfyjDH;Ae1jDC;EACE,WAAA;Cf4jDH;Ae7jDC;EACE,oBAAA;Cf+jDH;AehkDC;EACE,oBAAA;CfkkDH;AenkDC;EACE,WAAA;CfqkDH;AetkDC;EACE,oBAAA;CfwkDH;AezkDC;EACE,oBAAA;Cf2kDH;Ae5kDC;EACE,WAAA;Cf8kDH;Ae/kDC;EACE,oBAAA;CfilDH;AellDC;EACE,mBAAA;CfolDH;AehlDC;EACE,YAAA;CfklDH;AelmDC;EACE,WAAA;CfomDH;AermDC;EACE,mBAAA;CfumDH;AexmDC;EACE,mBAAA;Cf0mDH;Ae3mDC;EACE,UAAA;Cf6mDH;Ae9mDC;EACE,mBAAA;CfgnDH;AejnDC;EACE,mBAAA;CfmnDH;AepnDC;EACE,UAAA;CfsnDH;AevnDC;EACE,mBAAA;CfynDH;Ae1nDC;EACE,mBAAA;Cf4nDH;Ae7nDC;EACE,UAAA;Cf+nDH;AehoDC;EACE,mBAAA;CfkoDH;AenoDC;EACE,kBAAA;CfqoDH;AejoDC;EACE,WAAA;CfmoDH;AernDC;EACE,kBAAA;CfunDH;AexnDC;EACE,0BAAA;Cf0nDH;Ae3nDC;EACE,0BAAA;Cf6nDH;Ae9nDC;EACE,iBAAA;CfgoDH;AejoDC;EACE,0BAAA;CfmoDH;AepoDC;EACE,0BAAA;CfsoDH;AevoDC;EACE,iBAAA;CfyoDH;Ae1oDC;EACE,0BAAA;Cf4oDH;Ae7oDC;EACE,0BAAA;Cf+oDH;AehpDC;EACE,iBAAA;CfkpDH;AenpDC;EACE,0BAAA;CfqpDH;AetpDC;EACE,yBAAA;CfwpDH;AezpDC;EACE,gBAAA;Cf2pDH;Aa3pDD;EElCI;IACE,YAAA;GfgsDH;EezrDD;IACE,YAAA;Gf2rDD;Ee5rDD;IACE,oBAAA;Gf8rDD;Ee/rDD;IACE,oBAAA;GfisDD;EelsDD;IACE,WAAA;GfosDD;EersDD;IACE,oBAAA;GfusDD;EexsDD;IACE,oBAAA;Gf0sDD;Ee3sDD;IACE,WAAA;Gf6sDD;Ee9sDD;IACE,oBAAA;GfgtDD;EejtDD;IACE,oBAAA;GfmtDD;EeptDD;IACE,WAAA;GfstDD;EevtDD;IACE,oBAAA;GfytDD;Ee1tDD;IACE,mBAAA;Gf4tDD;Ee9sDD;IACE,YAAA;GfgtDD;EejtDD;IACE,oBAAA;GfmtDD;EeptDD;IACE,oBAAA;GfstDD;EevtDD;IACE,WAAA;GfytDD;Ee1tDD;IACE,oBAAA;Gf4tDD;Ee7tDD;IACE,oBAAA;Gf+tDD;EehuDD;IACE,WAAA;GfkuDD;EenuDD;IACE,oBAAA;GfquDD;EetuDD;IACE,oBAAA;GfwuDD;EezuDD;IACE,WAAA;Gf2uDD;Ee5uDD;IACE,oBAAA;Gf8uDD;Ee/uDD;IACE,mBAAA;GfivDD;Ee7uDD;IACE,YAAA;Gf+uDD;Ee/vDD;IACE,WAAA;GfiwDD;EelwDD;IACE,mBAAA;GfowDD;EerwDD;IACE,mBAAA;GfuwDD;EexwDD;IACE,UAAA;Gf0wDD;Ee3wDD;IACE,mBAAA;Gf6wDD;Ee9wDD;IACE,mBAAA;GfgxDD;EejxDD;IACE,UAAA;GfmxDD;EepxDD;IACE,mBAAA;GfsxDD;EevxDD;IACE,mBAAA;GfyxDD;Ee1xDD;IACE,UAAA;Gf4xDD;Ee7xDD;IACE,mBAAA;Gf+xDD;EehyDD;IACE,kBAAA;GfkyDD;Ee9xDD;IACE,WAAA;GfgyDD;EelxDD;IACE,kBAAA;GfoxDD;EerxDD;IACE,0BAAA;GfuxDD;EexxDD;IACE,0BAAA;Gf0xDD;Ee3xDD;IACE,iBAAA;Gf6xDD;Ee9xDD;IACE,0BAAA;GfgyDD;EejyDD;IACE,0BAAA;GfmyDD;EepyDD;IACE,iBAAA;GfsyDD;EevyDD;IACE,0BAAA;GfyyDD;Ee1yDD;IACE,0BAAA;Gf4yDD;Ee7yDD;IACE,iBAAA;Gf+yDD;EehzDD;IACE,0BAAA;GfkzDD;EenzDD;IACE,yBAAA;GfqzDD;EetzDD;IACE,gBAAA;GfwzDD;CACF;AahzDD;EE3CI;IACE,YAAA;Gf81DH;Eev1DD;IACE,YAAA;Gfy1DD;Ee11DD;IACE,oBAAA;Gf41DD;Ee71DD;IACE,oBAAA;Gf+1DD;Eeh2DD;IACE,WAAA;Gfk2DD;Een2DD;IACE,oBAAA;Gfq2DD;Eet2DD;IACE,oBAAA;Gfw2DD;Eez2DD;IACE,WAAA;Gf22DD;Ee52DD;IACE,oBAAA;Gf82DD;Ee/2DD;IACE,oBAAA;Gfi3DD;Eel3DD;IACE,WAAA;Gfo3DD;Eer3DD;IACE,oBAAA;Gfu3DD;Eex3DD;IACE,mBAAA;Gf03DD;Ee52DD;IACE,YAAA;Gf82DD;Ee/2DD;IACE,oBAAA;Gfi3DD;Eel3DD;IACE,oBAAA;Gfo3DD;Eer3DD;IACE,WAAA;Gfu3DD;Eex3DD;IACE,oBAAA;Gf03DD;Ee33DD;IACE,oBAAA;Gf63DD;Ee93DD;IACE,WAAA;Gfg4DD;Eej4DD;IACE,oBAAA;Gfm4DD;Eep4DD;IACE,oBAAA;Gfs4DD;Eev4DD;IACE,WAAA;Gfy4DD;Ee14DD;IACE,oBAAA;Gf44DD;Ee74DD;IACE,mBAAA;Gf+4DD;Ee34DD;IACE,YAAA;Gf64DD;Ee75DD;IACE,WAAA;Gf+5DD;Eeh6DD;IACE,mBAAA;Gfk6DD;Een6DD;IACE,mBAAA;Gfq6DD;Eet6DD;IACE,UAAA;Gfw6DD;Eez6DD;IACE,mBAAA;Gf26DD;Ee56DD;IACE,mBAAA;Gf86DD;Ee/6DD;IACE,UAAA;Gfi7DD;Eel7DD;IACE,mBAAA;Gfo7DD;Eer7DD;IACE,mBAAA;Gfu7DD;Eex7DD;IACE,UAAA;Gf07DD;Ee37DD;IACE,mBAAA;Gf67DD;Ee97DD;IACE,kBAAA;Gfg8DD;Ee57DD;IACE,WAAA;Gf87DD;Eeh7DD;IACE,kBAAA;Gfk7DD;Een7DD;IACE,0BAAA;Gfq7DD;Eet7DD;IACE,0BAAA;Gfw7DD;Eez7DD;IACE,iBAAA;Gf27DD;Ee57DD;IACE,0BAAA;Gf87DD;Ee/7DD;IACE,0BAAA;Gfi8DD;Eel8DD;IACE,iBAAA;Gfo8DD;Eer8DD;IACE,0BAAA;Gfu8DD;Eex8DD;IACE,0BAAA;Gf08DD;Ee38DD;IACE,iBAAA;Gf68DD;Ee98DD;IACE,0BAAA;Gfg9DD;Eej9DD;IACE,yBAAA;Gfm9DD;Eep9DD;IACE,gBAAA;Gfs9DD;CACF;Aa38DD;EE9CI;IACE,YAAA;Gf4/DH;Eer/DD;IACE,YAAA;Gfu/DD;Eex/DD;IACE,oBAAA;Gf0/DD;Ee3/DD;IACE,oBAAA;Gf6/DD;Ee9/DD;IACE,WAAA;GfggED;EejgED;IACE,oBAAA;GfmgED;EepgED;IACE,oBAAA;GfsgED;EevgED;IACE,WAAA;GfygED;Ee1gED;IACE,oBAAA;Gf4gED;Ee7gED;IACE,oBAAA;Gf+gED;EehhED;IACE,WAAA;GfkhED;EenhED;IACE,oBAAA;GfqhED;EethED;IACE,mBAAA;GfwhED;Ee1gED;IACE,YAAA;Gf4gED;Ee7gED;IACE,oBAAA;Gf+gED;EehhED;IACE,oBAAA;GfkhED;EenhED;IACE,WAAA;GfqhED;EethED;IACE,oBAAA;GfwhED;EezhED;IACE,oBAAA;Gf2hED;Ee5hED;IACE,WAAA;Gf8hED;Ee/hED;IACE,oBAAA;GfiiED;EeliED;IACE,oBAAA;GfoiED;EeriED;IACE,WAAA;GfuiED;EexiED;IACE,oBAAA;Gf0iED;Ee3iED;IACE,mBAAA;Gf6iED;EeziED;IACE,YAAA;Gf2iED;Ee3jED;IACE,WAAA;Gf6jED;Ee9jED;IACE,mBAAA;GfgkED;EejkED;IACE,mBAAA;GfmkED;EepkED;IACE,UAAA;GfskED;EevkED;IACE,mBAAA;GfykED;Ee1kED;IACE,mBAAA;Gf4kED;Ee7kED;IACE,UAAA;Gf+kED;EehlED;IACE,mBAAA;GfklED;EenlED;IACE,mBAAA;GfqlED;EetlED;IACE,UAAA;GfwlED;EezlED;IACE,mBAAA;Gf2lED;Ee5lED;IACE,kBAAA;Gf8lED;Ee1lED;IACE,WAAA;Gf4lED;Ee9kED;IACE,kBAAA;GfglED;EejlED;IACE,0BAAA;GfmlED;EeplED;IACE,0BAAA;GfslED;EevlED;IACE,iBAAA;GfylED;Ee1lED;IACE,0BAAA;Gf4lED;Ee7lED;IACE,0BAAA;Gf+lED;EehmED;IACE,iBAAA;GfkmED;EenmED;IACE,0BAAA;GfqmED;EetmED;IACE,0BAAA;GfwmED;EezmED;IACE,iBAAA;Gf2mED;Ee5mED;IACE,0BAAA;Gf8mED;Ee/mED;IACE,yBAAA;GfinED;EelnED;IACE,gBAAA;GfonED;CACF;AgBxrED;EACE,8BAAA;ChB0rED;AgBxrED;EACE,iBAAA;EACA,oBAAA;EACA,eAAA;EACA,iBAAA;ChB0rED;AgBxrED;EACE,iBAAA;ChB0rED;AgBprED;EACE,YAAA;EACA,gBAAA;EACA,oBAAA;ChBsrED;AgBzrED;;;;;;EAWQ,aAAA;EACA,wBAAA;EACA,oBAAA;EACA,8BAAA;ChBsrEP;AgBpsED;EAoBI,uBAAA;EACA,iCAAA;ChBmrEH;AgBxsED;;;;;;EA8BQ,cAAA;ChBkrEP;AgBhtED;EAoCI,8BAAA;ChB+qEH;AgBntED;EAyCI,0BAAA;ChB6qEH;AgBtqED;;;;;;EAOQ,aAAA;ChBuqEP;AgB5pED;EACE,0BAAA;ChB8pED;AgB/pED;;;;;;EAQQ,0BAAA;ChB+pEP;AgBvqED;;EAeM,yBAAA;ChB4pEL;AgBlpED;EAEI,0BAAA;ChBmpEH;AgB1oED;EAEI,0BAAA;ChB2oEH;AgBloED;EACE,iBAAA;EACA,YAAA;EACA,sBAAA;ChBooED;AgB/nEG;;EACE,iBAAA;EACA,YAAA;EACA,oBAAA;ChBkoEL;AiB9wEC;;;;;;;;;;;;EAOI,0BAAA;CjBqxEL;AiB/wEC;;;;;EAMI,0BAAA;CjBgxEL;AiBnyEC;;;;;;;;;;;;EAOI,0BAAA;CjB0yEL;AiBpyEC;;;;;EAMI,0BAAA;CjBqyEL;AiBxzEC;;;;;;;;;;;;EAOI,0BAAA;CjB+zEL;AiBzzEC;;;;;EAMI,0BAAA;CjB0zEL;AiB70EC;;;;;;;;;;;;EAOI,0BAAA;CjBo1EL;AiB90EC;;;;;EAMI,0BAAA;CjB+0EL;AiBl2EC;;;;;;;;;;;;EAOI,0BAAA;CjBy2EL;AiBn2EC;;;;;EAMI,0BAAA;CjBo2EL;AgBltED;EACE,iBAAA;EACA,kBAAA;ChBotED;AgBvpED;EAAA;IA1DI,YAAA;IACA,oBAAA;IACA,mBAAA;IACA,6CAAA;IACA,0BAAA;GhBqtED;EgB/pEH;IAlDM,iBAAA;GhBotEH;EgBlqEH;;;;;;IAzCY,oBAAA;GhBmtET;EgB1qEH;IAjCM,UAAA;GhB8sEH;EgB7qEH;;;;;;IAxBY,eAAA;GhB6sET;EgBrrEH;;;;;;IApBY,gBAAA;GhBitET;EgB7rEH;;;;IAPY,iBAAA;GhB0sET;CACF;AkBp6ED;EACE,WAAA;EACA,UAAA;EACA,UAAA;EAIA,aAAA;ClBm6ED;AkBh6ED;EACE,eAAA;EACA,YAAA;EACA,WAAA;EACA,oBAAA;EACA,gBAAA;EACA,qBAAA;EACA,eAAA;EACA,UAAA;EACA,iCAAA;ClBk6ED;AkB/5ED;EACE,sBAAA;EACA,gBAAA;EACA,mBAAA;EACA,kBAAA;ClBi6ED;AkBt5ED;Eb4BE,+BAAA;EACG,4BAAA;EACK,uBAAA;CL63ET;AkBt5ED;;EAEE,gBAAA;EACA,mBAAA;EACA,oBAAA;ClBw5ED;AkBr5ED;EACE,eAAA;ClBu5ED;AkBn5ED;EACE,eAAA;EACA,YAAA;ClBq5ED;AkBj5ED;;EAEE,aAAA;ClBm5ED;AkB/4ED;;;EZvEE,qBAAA;EAEA,2CAAA;EACA,qBAAA;CN09ED;AkB/4ED;EACE,eAAA;EACA,iBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;ClBi5ED;AkBv3ED;EACE,eAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,0BAAA;EACA,uBAAA;EACA,0BAAA;EACA,mBAAA;EbxDA,yDAAA;EACQ,iDAAA;EAyHR,uFAAA;EACK,0EAAA;EACG,uEAAA;CL0zET;AmBl8EC;EACE,sBAAA;EACA,WAAA;EdUF,uFAAA;EACQ,+EAAA;CL27ET;AK15EC;EACE,eAAA;EACA,WAAA;CL45EH;AK15EC;EAA0B,eAAA;CL65E3B;AK55EC;EAAgC,eAAA;CL+5EjC;AkB/3EC;;;EAGE,0BAAA;EACA,WAAA;ClBi4EH;AkB93EC;;EAEE,oBAAA;ClBg4EH;AkB53EC;EACE,aAAA;ClB83EH;AkBl3ED;EACE,yBAAA;ClBo3ED;AkB50ED;EAtBI;;;;IACE,kBAAA;GlBw2EH;EkBr2EC;;;;;;;;IAEE,kBAAA;GlB62EH;EkB12EC;;;;;;;;IAEE,kBAAA;GlBk3EH;CACF;AkBx2ED;EACE,oBAAA;ClB02ED;AkBl2ED;;EAEE,mBAAA;EACA,eAAA;EACA,iBAAA;EACA,oBAAA;ClBo2ED;AkBz2ED;;EAQI,iBAAA;EACA,mBAAA;EACA,iBAAA;EACA,oBAAA;EACA,gBAAA;ClBq2EH;AkBl2ED;;;;EAIE,mBAAA;EACA,mBAAA;EACA,mBAAA;ClBo2ED;AkBj2ED;;EAEE,iBAAA;ClBm2ED;AkB/1ED;;EAEE,mBAAA;EACA,sBAAA;EACA,mBAAA;EACA,iBAAA;EACA,uBAAA;EACA,oBAAA;EACA,gBAAA;ClBi2ED;AkB/1ED;;EAEE,cAAA;EACA,kBAAA;ClBi2ED;AkBx1EC;;;;;;EAGE,oBAAA;ClB61EH;AkBv1EC;;;;EAEE,oBAAA;ClB21EH;AkBr1EC;;;;EAGI,oBAAA;ClBw1EL;AkB70ED;EAEE,iBAAA;EACA,oBAAA;EAEA,iBAAA;EACA,iBAAA;ClB60ED;AkB30EC;;EAEE,gBAAA;EACA,iBAAA;ClB60EH;AkBh0ED;EC7PE,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CnBgkFD;AmB9jFC;EACE,aAAA;EACA,kBAAA;CnBgkFH;AmB7jFC;;EAEE,aAAA;CnB+jFH;AkB50ED;EAEI,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;ClB60EH;AkBn1ED;EASI,aAAA;EACA,kBAAA;ClB60EH;AkBv1ED;;EAcI,aAAA;ClB60EH;AkB31ED;EAiBI,aAAA;EACA,iBAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;ClB60EH;AkBz0ED;ECzRE,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CnBqmFD;AmBnmFC;EACE,aAAA;EACA,kBAAA;CnBqmFH;AmBlmFC;;EAEE,aAAA;CnBomFH;AkBr1ED;EAEI,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;ClBs1EH;AkB51ED;EASI,aAAA;EACA,kBAAA;ClBs1EH;AkBh2ED;;EAcI,aAAA;ClBs1EH;AkBp2ED;EAiBI,aAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;ClBs1EH;AkB70ED;EAEE,mBAAA;ClB80ED;AkBh1ED;EAMI,sBAAA;ClB60EH;AkBz0ED;EACE,mBAAA;EACA,OAAA;EACA,SAAA;EACA,WAAA;EACA,eAAA;EACA,YAAA;EACA,aAAA;EACA,kBAAA;EACA,mBAAA;EACA,qBAAA;ClB20ED;AkBz0ED;;;EAGE,YAAA;EACA,aAAA;EACA,kBAAA;ClB20ED;AkBz0ED;;;EAGE,YAAA;EACA,aAAA;EACA,kBAAA;ClB20ED;AkBv0ED;;;;;;;;;;ECpZI,eAAA;CnBuuFH;AkBn1ED;EChZI,sBAAA;Ed+CF,yDAAA;EACQ,iDAAA;CLwrFT;AmBtuFG;EACE,sBAAA;Ed4CJ,0EAAA;EACQ,kEAAA;CL6rFT;AkB71ED;ECtYI,eAAA;EACA,sBAAA;EACA,0BAAA;CnBsuFH;AkBl2ED;EChYI,eAAA;CnBquFH;AkBl2ED;;;;;;;;;;ECvZI,eAAA;CnBqwFH;AkB92ED;ECnZI,sBAAA;Ed+CF,yDAAA;EACQ,iDAAA;CLstFT;AmBpwFG;EACE,sBAAA;Ed4CJ,0EAAA;EACQ,kEAAA;CL2tFT;AkBx3ED;ECzYI,eAAA;EACA,sBAAA;EACA,0BAAA;CnBowFH;AkB73ED;ECnYI,eAAA;CnBmwFH;AkB73ED;;;;;;;;;;EC1ZI,eAAA;CnBmyFH;AkBz4ED;ECtZI,sBAAA;Ed+CF,yDAAA;EACQ,iDAAA;CLovFT;AmBlyFG;EACE,sBAAA;Ed4CJ,0EAAA;EACQ,kEAAA;CLyvFT;AkBn5ED;EC5YI,eAAA;EACA,sBAAA;EACA,0BAAA;CnBkyFH;AkBx5ED;ECtYI,eAAA;CnBiyFH;AkBp5EC;EACG,UAAA;ClBs5EJ;AkBp5EC;EACG,OAAA;ClBs5EJ;AkB54ED;EACE,eAAA;EACA,gBAAA;EACA,oBAAA;EACA,eAAA;ClB84ED;AkB3zED;EAAA;IA9DM,sBAAA;IACA,iBAAA;IACA,uBAAA;GlB63EH;EkBj0EH;IAvDM,sBAAA;IACA,YAAA;IACA,uBAAA;GlB23EH;EkBt0EH;IAhDM,sBAAA;GlBy3EH;EkBz0EH;IA5CM,sBAAA;IACA,uBAAA;GlBw3EH;EkB70EH;;;IAtCQ,YAAA;GlBw3EL;EkBl1EH;IAhCM,YAAA;GlBq3EH;EkBr1EH;IA5BM,iBAAA;IACA,uBAAA;GlBo3EH;EkBz1EH;;IApBM,sBAAA;IACA,cAAA;IACA,iBAAA;IACA,uBAAA;GlBi3EH;EkBh2EH;;IAdQ,gBAAA;GlBk3EL;EkBp2EH;;IATM,mBAAA;IACA,eAAA;GlBi3EH;EkBz2EH;IAHM,OAAA;GlB+2EH;CACF;AkBr2ED;;;;EASI,cAAA;EACA,iBAAA;EACA,iBAAA;ClBk2EH;AkB72ED;;EAiBI,iBAAA;ClBg2EH;AkBj3ED;EJhhBE,mBAAA;EACA,oBAAA;Cdo4FD;AkB90EC;EAAA;IAVI,kBAAA;IACA,iBAAA;IACA,iBAAA;GlB41EH;CACF;AkB53ED;EAwCI,YAAA;ClBu1EH;AkBz0EC;EAAA;IAJM,yBAAA;IACA,gBAAA;GlBi1EL;CACF;AkBv0EC;EAAA;IAJM,iBAAA;IACA,gBAAA;GlB+0EL;CACF;AoBl6FD;EACE,sBAAA;EACA,iBAAA;EACA,oBAAA;EACA,mBAAA;EACA,uBAAA;EACA,+BAAA;MAAA,2BAAA;EACA,gBAAA;EACA,uBAAA;EACA,8BAAA;EACA,oBAAA;EC6CA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,mBAAA;EhB4JA,0BAAA;EACG,uBAAA;EACC,sBAAA;EACI,kBAAA;CL6tFT;AoBr6FG;;;;;;EdrBF,qBAAA;EAEA,2CAAA;EACA,qBAAA;CNi8FD;AoBz6FC;;;EAGE,eAAA;EACA,sBAAA;CpB26FH;AoBx6FC;;EAEE,WAAA;EACA,uBAAA;Ef2BF,yDAAA;EACQ,iDAAA;CLg5FT;AoBx6FC;;;EAGE,oBAAA;EE7CF,cAAA;EAGA,0BAAA;EjB8DA,yBAAA;EACQ,iBAAA;CLy5FT;AoBx6FG;;EAEE,qBAAA;CpB06FL;AoBj6FD;EC3DE,eAAA;EACA,0BAAA;EACA,sBAAA;CrB+9FD;AqB79FC;;EAEE,eAAA;EACA,0BAAA;EACI,sBAAA;CrB+9FP;AqB79FC;EACE,eAAA;EACA,0BAAA;EACI,sBAAA;CrB+9FP;AqB79FC;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrB+9FP;AqB79FG;;;;;;;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBq+FT;AqBl+FC;;;EAGE,uBAAA;CrBo+FH;AqB/9FG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACI,sBAAA;CrB6+FT;AoB/9FD;ECTI,eAAA;EACA,0BAAA;CrB2+FH;AoBh+FD;EC9DE,eAAA;EACA,0BAAA;EACA,sBAAA;CrBiiGD;AqB/hGC;;EAEE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBiiGP;AqB/hGC;EACE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBiiGP;AqB/hGC;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBiiGP;AqB/hGG;;;;;;;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBuiGT;AqBpiGC;;;EAGE,uBAAA;CrBsiGH;AqBjiGG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACI,sBAAA;CrB+iGT;AoB9hGD;ECZI,eAAA;EACA,0BAAA;CrB6iGH;AoB9hGD;EClEE,eAAA;EACA,0BAAA;EACA,sBAAA;CrBmmGD;AqBjmGC;;EAEE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBmmGP;AqBjmGC;EACE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBmmGP;AqBjmGC;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBmmGP;AqBjmGG;;;;;;;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBymGT;AqBtmGC;;;EAGE,uBAAA;CrBwmGH;AqBnmGG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACI,sBAAA;CrBinGT;AoB5lGD;EChBI,eAAA;EACA,0BAAA;CrB+mGH;AoB5lGD;ECtEE,eAAA;EACA,0BAAA;EACA,sBAAA;CrBqqGD;AqBnqGC;;EAEE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBqqGP;AqBnqGC;EACE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBqqGP;AqBnqGC;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBqqGP;AqBnqGG;;;;;;;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrB2qGT;AqBxqGC;;;EAGE,uBAAA;CrB0qGH;AqBrqGG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACI,sBAAA;CrBmrGT;AoB1pGD;ECpBI,eAAA;EACA,0BAAA;CrBirGH;AoB1pGD;EC1EE,eAAA;EACA,0BAAA;EACA,sBAAA;CrBuuGD;AqBruGC;;EAEE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBuuGP;AqBruGC;EACE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBuuGP;AqBruGC;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrBuuGP;AqBruGG;;;;;;;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrB6uGT;AqB1uGC;;;EAGE,uBAAA;CrB4uGH;AqBvuGG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACI,sBAAA;CrBqvGT;AoBxtGD;ECxBI,eAAA;EACA,0BAAA;CrBmvGH;AoBxtGD;EC9EE,eAAA;EACA,0BAAA;EACA,sBAAA;CrByyGD;AqBvyGC;;EAEE,eAAA;EACA,0BAAA;EACI,sBAAA;CrByyGP;AqBvyGC;EACE,eAAA;EACA,0BAAA;EACI,sBAAA;CrByyGP;AqBvyGC;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrByyGP;AqBvyGG;;;;;;;;;EAGE,eAAA;EACA,0BAAA;EACI,sBAAA;CrB+yGT;AqB5yGC;;;EAGE,uBAAA;CrB8yGH;AqBzyGG;;;;;;;;;;;;;;;;;;EAME,0BAAA;EACI,sBAAA;CrBuzGT;AoBtxGD;EC5BI,eAAA;EACA,0BAAA;CrBqzGH;AoBjxGD;EACE,eAAA;EACA,oBAAA;EACA,iBAAA;CpBmxGD;AoBjxGC;;;;;EAKE,8BAAA;EfnCF,yBAAA;EACQ,iBAAA;CLuzGT;AoBlxGC;;;;EAIE,0BAAA;CpBoxGH;AoBlxGC;;EAEE,eAAA;EACA,2BAAA;EACA,8BAAA;CpBoxGH;AoBhxGG;;;;EAEE,eAAA;EACA,sBAAA;CpBoxGL;AoB3wGD;;ECrEE,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CrBo1GD;AoB9wGD;;ECzEE,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CrB21GD;AoBjxGD;;EC7EE,iBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CrBk2GD;AoBhxGD;EACE,eAAA;EACA,YAAA;CpBkxGD;AoB9wGD;EACE,gBAAA;CpBgxGD;AoBzwGC;;;EACE,YAAA;CpB6wGH;AuBv6GD;EACE,WAAA;ElBoLA,yCAAA;EACK,oCAAA;EACG,iCAAA;CLsvGT;AuB16GC;EACE,WAAA;CvB46GH;AuBx6GD;EACE,cAAA;CvB06GD;AuBx6GC;EAAY,eAAA;CvB26Gb;AuB16GC;EAAY,mBAAA;CvB66Gb;AuB56GC;EAAY,yBAAA;CvB+6Gb;AuB56GD;EACE,mBAAA;EACA,UAAA;EACA,iBAAA;ElBuKA,gDAAA;EACQ,2CAAA;KAAA,wCAAA;EAOR,mCAAA;EACQ,8BAAA;KAAA,2BAAA;EAGR,yCAAA;EACQ,oCAAA;KAAA,iCAAA;CLgwGT;AwB18GD;EACE,sBAAA;EACA,SAAA;EACA,UAAA;EACA,iBAAA;EACA,uBAAA;EACA,uBAAA;EACA,yBAAA;EACA,oCAAA;EACA,mCAAA;CxB48GD;AwBx8GD;;EAEE,mBAAA;CxB08GD;AwBt8GD;EACE,WAAA;CxBw8GD;AwBp8GD;EACE,mBAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,YAAA;EACA,iBAAA;EACA,eAAA;EACA,gBAAA;EACA,iBAAA;EACA,gBAAA;EACA,iBAAA;EACA,0BAAA;EACA,0BAAA;EACA,sCAAA;EACA,mBAAA;EnBsBA,oDAAA;EACQ,4CAAA;EmBrBR,qCAAA;UAAA,6BAAA;CxBu8GD;AwBl8GC;EACE,SAAA;EACA,WAAA;CxBo8GH;AwB79GD;ECzBE,YAAA;EACA,cAAA;EACA,iBAAA;EACA,0BAAA;CzBy/GD;AwBn+GD;EAmCI,eAAA;EACA,kBAAA;EACA,YAAA;EACA,oBAAA;EACA,wBAAA;EACA,eAAA;EACA,oBAAA;CxBm8GH;AwB77GC;;EAEE,sBAAA;EACA,eAAA;EACA,0BAAA;CxB+7GH;AwBz7GC;;;EAGE,eAAA;EACA,sBAAA;EACA,WAAA;EACA,0BAAA;CxB27GH;AwBl7GC;;;EAGE,eAAA;CxBo7GH;AwBh7GC;;EAEE,sBAAA;EACA,8BAAA;EACA,uBAAA;EE3GF,oEAAA;EF6GE,oBAAA;CxBk7GH;AwB76GD;EAGI,eAAA;CxB66GH;AwBh7GD;EAQI,WAAA;CxB26GH;AwBn6GD;EACE,WAAA;EACA,SAAA;CxBq6GD;AwB75GD;EACE,QAAA;EACA,YAAA;CxB+5GD;AwB35GD;EACE,eAAA;EACA,kBAAA;EACA,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,oBAAA;CxB65GD;AwBz5GD;EACE,gBAAA;EACA,QAAA;EACA,SAAA;EACA,UAAA;EACA,OAAA;EACA,aAAA;CxB25GD;AwBv5GD;EACE,SAAA;EACA,WAAA;CxBy5GD;AwBj5GD;;EAII,cAAA;EACA,0BAAA;EACA,4BAAA;EACA,YAAA;CxBi5GH;AwBx5GD;;EAWI,UAAA;EACA,aAAA;EACA,mBAAA;CxBi5GH;AwB53GD;EAXE;IApEA,WAAA;IACA,SAAA;GxB+8GC;EwB54GD;IA1DA,QAAA;IACA,YAAA;GxBy8GC;CACF;A2BzlHD;;EAEE,mBAAA;EACA,sBAAA;EACA,uBAAA;C3B2lHD;A2B/lHD;;EAMI,mBAAA;EACA,YAAA;C3B6lHH;A2B3lHG;;;;;;;;EAIE,WAAA;C3BimHL;A2B3lHD;;;;EAKI,kBAAA;C3B4lHH;A2BvlHD;EACE,kBAAA;C3BylHD;A2B1lHD;;;EAOI,YAAA;C3BwlHH;A2B/lHD;;;EAYI,iBAAA;C3BwlHH;A2BplHD;EACE,iBAAA;C3BslHD;A2BllHD;EACE,eAAA;C3BolHD;A2BnlHC;EClDA,8BAAA;EACG,2BAAA;C5BwoHJ;A2BllHD;;EC/CE,6BAAA;EACG,0BAAA;C5BqoHJ;A2BjlHD;EACE,YAAA;C3BmlHD;A2BjlHD;EACE,iBAAA;C3BmlHD;A2BjlHD;;ECnEE,8BAAA;EACG,2BAAA;C5BwpHJ;A2BhlHD;ECjEE,6BAAA;EACG,0BAAA;C5BopHJ;A2B/kHD;;EAEE,WAAA;C3BilHD;A2BhkHD;EACE,kBAAA;EACA,mBAAA;C3BkkHD;A2BhkHD;EACE,mBAAA;EACA,oBAAA;C3BkkHD;A2B7jHD;EtB/CE,yDAAA;EACQ,iDAAA;CL+mHT;A2B7jHC;EtBnDA,yBAAA;EACQ,iBAAA;CLmnHT;A2B1jHD;EACE,eAAA;C3B4jHD;A2BzjHD;EACE,wBAAA;EACA,uBAAA;C3B2jHD;A2BxjHD;EACE,wBAAA;C3B0jHD;A2BnjHD;;;EAII,eAAA;EACA,YAAA;EACA,YAAA;EACA,gBAAA;C3BojHH;A2B3jHD;EAcM,YAAA;C3BgjHL;A2B9jHD;;;;EAsBI,iBAAA;EACA,eAAA;C3B8iHH;A2BziHC;EACE,iBAAA;C3B2iHH;A2BziHC;EACE,6BAAA;ECpKF,8BAAA;EACC,6BAAA;C5BgtHF;A2B1iHC;EACE,+BAAA;EChLF,2BAAA;EACC,0BAAA;C5B6tHF;A2B1iHD;EACE,iBAAA;C3B4iHD;A2B1iHD;;EC/KE,8BAAA;EACC,6BAAA;C5B6tHF;A2BziHD;EC7LE,2BAAA;EACC,0BAAA;C5ByuHF;A2BriHD;EACE,eAAA;EACA,YAAA;EACA,oBAAA;EACA,0BAAA;C3BuiHD;A2B3iHD;;EAOI,YAAA;EACA,oBAAA;EACA,UAAA;C3BwiHH;A2BjjHD;EAYI,YAAA;C3BwiHH;A2BpjHD;EAgBI,WAAA;C3BuiHH;A2BthHD;;;;EAKM,mBAAA;EACA,uBAAA;EACA,qBAAA;C3BuhHL;A6BjwHD;EACE,mBAAA;EACA,eAAA;EACA,0BAAA;C7BmwHD;A6BhwHC;EACE,YAAA;EACA,gBAAA;EACA,iBAAA;C7BkwHH;A6B3wHD;EAeI,mBAAA;EACA,WAAA;EAKA,YAAA;EAEA,YAAA;EACA,iBAAA;C7B0vHH;A6BjvHD;;;EV8BE,aAAA;EACA,mBAAA;EACA,gBAAA;EACA,uBAAA;EACA,mBAAA;CnBwtHD;AmBttHC;;;EACE,aAAA;EACA,kBAAA;CnB0tHH;AmBvtHC;;;;;;EAEE,aAAA;CnB6tHH;A6BnwHD;;;EVyBE,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;CnB+uHD;AmB7uHC;;;EACE,aAAA;EACA,kBAAA;CnBivHH;AmB9uHC;;;;;;EAEE,aAAA;CnBovHH;A6BjxHD;;;EAGE,oBAAA;C7BmxHD;A6BjxHC;;;EACE,iBAAA;C7BqxHH;A6BjxHD;;EAEE,UAAA;EACA,oBAAA;EACA,uBAAA;C7BmxHD;A6B9wHD;EACE,kBAAA;EACA,gBAAA;EACA,oBAAA;EACA,eAAA;EACA,eAAA;EACA,mBAAA;EACA,0BAAA;EACA,0BAAA;EACA,mBAAA;C7BgxHD;A6B7wHC;EACE,kBAAA;EACA,gBAAA;EACA,mBAAA;C7B+wHH;A6B7wHC;EACE,mBAAA;EACA,gBAAA;EACA,mBAAA;C7B+wHH;A6BnyHD;;EA0BI,cAAA;C7B6wHH;A6BxwHD;;;;;;;EDhGE,8BAAA;EACG,2BAAA;C5Bi3HJ;A6BzwHD;EACE,gBAAA;C7B2wHD;A6BzwHD;;;;;;;EDpGE,6BAAA;EACG,0BAAA;C5Bs3HJ;A6B1wHD;EACE,eAAA;C7B4wHD;A6BvwHD;EACE,mBAAA;EAGA,aAAA;EACA,oBAAA;C7BuwHD;A6B5wHD;EAUI,mBAAA;C7BqwHH;A6B/wHD;EAYM,kBAAA;C7BswHL;A6BnwHG;;;EAGE,WAAA;C7BqwHL;A6BhwHC;;EAGI,mBAAA;C7BiwHL;A6B9vHC;;EAGI,WAAA;EACA,kBAAA;C7B+vHL;A8B15HD;EACE,iBAAA;EACA,gBAAA;EACA,iBAAA;C9B45HD;A8B/5HD;EAOI,mBAAA;EACA,eAAA;C9B25HH;A8Bn6HD;EAWM,mBAAA;EACA,eAAA;EACA,mBAAA;C9B25HL;A8B15HK;;EAEE,sBAAA;EACA,0BAAA;C9B45HP;A8Bv5HG;EACE,eAAA;C9By5HL;A8Bv5HK;;EAEE,eAAA;EACA,sBAAA;EACA,8BAAA;EACA,oBAAA;C9By5HP;A8Bl5HG;;;EAGE,0BAAA;EACA,sBAAA;C9Bo5HL;A8B77HD;ELHE,YAAA;EACA,cAAA;EACA,iBAAA;EACA,0BAAA;CzBm8HD;A8Bn8HD;EA0DI,gBAAA;C9B44HH;A8Bn4HD;EACE,iCAAA;C9Bq4HD;A8Bt4HD;EAGI,YAAA;EAEA,oBAAA;C9Bq4HH;A8B14HD;EASM,kBAAA;EACA,wBAAA;EACA,8BAAA;EACA,2BAAA;C9Bo4HL;A8Bn4HK;EACE,sCAAA;C9Bq4HP;A8B/3HK;;;EAGE,eAAA;EACA,0BAAA;EACA,0BAAA;EACA,iCAAA;EACA,gBAAA;C9Bi4HP;A8B53HC;EAqDA,YAAA;EA8BA,iBAAA;C9B6yHD;A8Bh4HC;EAwDE,YAAA;C9B20HH;A8Bn4HC;EA0DI,mBAAA;EACA,mBAAA;C9B40HL;A8Bv4HC;EAgEE,UAAA;EACA,WAAA;C9B00HH;A8B9zHD;EAAA;IAPM,oBAAA;IACA,UAAA;G9By0HH;E8Bn0HH;IAJQ,iBAAA;G9B00HL;CACF;A8Bp5HC;EAuFE,gBAAA;EACA,mBAAA;C9Bg0HH;A8Bx5HC;;;EA8FE,0BAAA;C9B+zHH;A8BjzHD;EAAA;IATM,iCAAA;IACA,2BAAA;G9B8zHH;E8BtzHH;;;IAHM,6BAAA;G9B8zHH;CACF;A8B/5HD;EAEI,YAAA;C9Bg6HH;A8Bl6HD;EAMM,mBAAA;C9B+5HL;A8Br6HD;EASM,iBAAA;C9B+5HL;A8B15HK;;;EAGE,eAAA;EACA,0BAAA;C9B45HP;A8Bp5HD;EAEI,YAAA;C9Bq5HH;A8Bv5HD;EAIM,gBAAA;EACA,eAAA;C9Bs5HL;A8B14HD;EACE,YAAA;C9B44HD;A8B74HD;EAII,YAAA;C9B44HH;A8Bh5HD;EAMM,mBAAA;EACA,mBAAA;C9B64HL;A8Bp5HD;EAYI,UAAA;EACA,WAAA;C9B24HH;A8B/3HD;EAAA;IAPM,oBAAA;IACA,UAAA;G9B04HH;E8Bp4HH;IAJQ,iBAAA;G9B24HL;CACF;A8Bn4HD;EACE,iBAAA;C9Bq4HD;A8Bt4HD;EAKI,gBAAA;EACA,mBAAA;C9Bo4HH;A8B14HD;;;EAYI,0BAAA;C9Bm4HH;A8Br3HD;EAAA;IATM,iCAAA;IACA,2BAAA;G9Bk4HH;E8B13HH;;;IAHM,6BAAA;G9Bk4HH;CACF;A8Bz3HD;EAEI,cAAA;C9B03HH;A8B53HD;EAKI,eAAA;C9B03HH;A8Bj3HD;EAEE,iBAAA;EF3OA,2BAAA;EACC,0BAAA;C5B8lIF;A+BxlID;EACE,mBAAA;EACA,iBAAA;EACA,oBAAA;EACA,8BAAA;C/B0lID;A+BllID;EAAA;IAFI,mBAAA;G/BwlID;CACF;A+BzkID;EAAA;IAFI,YAAA;G/B+kID;CACF;A+BjkID;EACE,oBAAA;EACA,oBAAA;EACA,mBAAA;EACA,kCAAA;EACA,2DAAA;UAAA,mDAAA;EAEA,kCAAA;C/BkkID;A+BhkIC;EACE,iBAAA;C/BkkIH;A+BtiID;EAAA;IAxBI,YAAA;IACA,cAAA;IACA,yBAAA;YAAA,iBAAA;G/BkkID;E+BhkIC;IACE,0BAAA;IACA,wBAAA;IACA,kBAAA;IACA,6BAAA;G/BkkIH;E+B/jIC;IACE,oBAAA;G/BikIH;E+B5jIC;;;IAGE,gBAAA;IACA,iBAAA;G/B8jIH;CACF;A+B1jID;;EAGI,kBAAA;C/B2jIH;A+BtjIC;EAAA;;IAFI,kBAAA;G/B6jIH;CACF;A+BpjID;;;;EAII,oBAAA;EACA,mBAAA;C/BsjIH;A+BhjIC;EAAA;;;;IAHI,gBAAA;IACA,eAAA;G/B0jIH;CACF;A+B9iID;EACE,cAAA;EACA,sBAAA;C/BgjID;A+B3iID;EAAA;IAFI,iBAAA;G/BijID;CACF;A+B7iID;;EAEE,gBAAA;EACA,SAAA;EACA,QAAA;EACA,cAAA;C/B+iID;A+BziID;EAAA;;IAFI,iBAAA;G/BgjID;CACF;A+B9iID;EACE,OAAA;EACA,sBAAA;C/BgjID;A+B9iID;EACE,UAAA;EACA,iBAAA;EACA,sBAAA;C/BgjID;A+B1iID;EACE,YAAA;EACA,mBAAA;EACA,gBAAA;EACA,kBAAA;EACA,aAAA;C/B4iID;A+B1iIC;;EAEE,sBAAA;C/B4iIH;A+BrjID;EAaI,eAAA;C/B2iIH;A+BliID;EALI;;IAEE,mBAAA;G/B0iIH;CACF;A+BhiID;EACE,mBAAA;EACA,aAAA;EACA,mBAAA;EACA,kBAAA;EC9LA,gBAAA;EACA,mBAAA;ED+LA,8BAAA;EACA,uBAAA;EACA,8BAAA;EACA,mBAAA;C/BmiID;A+B/hIC;EACE,WAAA;C/BiiIH;A+B/iID;EAmBI,eAAA;EACA,YAAA;EACA,YAAA;EACA,mBAAA;C/B+hIH;A+BrjID;EAyBI,gBAAA;C/B+hIH;A+BzhID;EAAA;IAFI,cAAA;G/B+hID;CACF;A+BthID;EACE,oBAAA;C/BwhID;A+BzhID;EAII,kBAAA;EACA,qBAAA;EACA,kBAAA;C/BwhIH;A+B5/HC;EAAA;IAtBI,iBAAA;IACA,YAAA;IACA,YAAA;IACA,cAAA;IACA,8BAAA;IACA,UAAA;IACA,yBAAA;YAAA,iBAAA;G/BshIH;E+BtgID;;IAbM,2BAAA;G/BuhIL;E+B1gID;IAVM,kBAAA;G/BuhIL;E+BthIK;;IAEE,uBAAA;G/BwhIP;CACF;A+BtgID;EAAA;IAXI,YAAA;IACA,UAAA;G/BqhID;E+B3gIH;IAPM,YAAA;G/BqhIH;E+B9gIH;IALQ,kBAAA;IACA,qBAAA;G/BshIL;CACF;A+B3gID;EACE,mBAAA;EACA,oBAAA;EACA,mBAAA;EACA,kCAAA;EACA,qCAAA;E1B9NA,6FAAA;EACQ,qFAAA;E2B/DR,gBAAA;EACA,mBAAA;ChC4yID;AkB5xHD;EAAA;IA9DM,sBAAA;IACA,iBAAA;IACA,uBAAA;GlB81HH;EkBlyHH;IAvDM,sBAAA;IACA,YAAA;IACA,uBAAA;GlB41HH;EkBvyHH;IAhDM,sBAAA;GlB01HH;EkB1yHH;IA5CM,sBAAA;IACA,uBAAA;GlBy1HH;EkB9yHH;;;IAtCQ,YAAA;GlBy1HL;EkBnzHH;IAhCM,YAAA;GlBs1HH;EkBtzHH;IA5BM,iBAAA;IACA,uBAAA;GlBq1HH;EkB1zHH;;IApBM,sBAAA;IACA,cAAA;IACA,iBAAA;IACA,uBAAA;GlBk1HH;EkBj0HH;;IAdQ,gBAAA;GlBm1HL;EkBr0HH;;IATM,mBAAA;IACA,eAAA;GlBk1HH;EkB10HH;IAHM,OAAA;GlBg1HH;CACF;A+BpjIC;EAAA;IANI,mBAAA;G/B8jIH;E+B5jIG;IACE,iBAAA;G/B8jIL;CACF;A+B7iID;EAAA;IARI,YAAA;IACA,UAAA;IACA,eAAA;IACA,gBAAA;IACA,eAAA;IACA,kBAAA;I1BzPF,yBAAA;IACQ,iBAAA;GLmzIP;CACF;A+BnjID;EACE,cAAA;EHpUA,2BAAA;EACC,0BAAA;C5B03IF;A+BnjID;EACE,iBAAA;EHzUA,6BAAA;EACC,4BAAA;EAOD,8BAAA;EACC,6BAAA;C5By3IF;A+B/iID;EChVE,gBAAA;EACA,mBAAA;ChCk4ID;A+BhjIC;ECnVA,iBAAA;EACA,oBAAA;ChCs4ID;A+BjjIC;ECtVA,iBAAA;EACA,oBAAA;ChC04ID;A+B3iID;EChWE,iBAAA;EACA,oBAAA;ChC84ID;A+BviID;EAAA;IAJI,YAAA;IACA,kBAAA;IACA,mBAAA;G/B+iID;CACF;A+BlhID;EAhBE;IExWA,uBAAA;GjC84IC;E+BriID;IE5WA,wBAAA;IF8WE,oBAAA;G/BuiID;E+BziID;IAKI,gBAAA;G/BuiIH;CACF;A+B9hID;EACE,0BAAA;EACA,sBAAA;C/BgiID;A+BliID;EAKI,eAAA;C/BgiIH;A+B/hIG;;EAEE,eAAA;EACA,8BAAA;C/BiiIL;A+B1iID;EAcI,eAAA;C/B+hIH;A+B7iID;EAmBM,eAAA;C/B6hIL;A+B3hIK;;EAEE,eAAA;EACA,8BAAA;C/B6hIP;A+BzhIK;;;EAGE,eAAA;EACA,0BAAA;C/B2hIP;A+BvhIK;;;EAGE,eAAA;EACA,8BAAA;C/ByhIP;A+BjkID;EA8CI,sBAAA;C/BshIH;A+BrhIG;;EAEE,0BAAA;C/BuhIL;A+BxkID;EAoDM,0BAAA;C/BuhIL;A+B3kID;;EA0DI,sBAAA;C/BqhIH;A+B9gIK;;;EAGE,0BAAA;EACA,eAAA;C/BghIP;A+B/+HC;EAAA;IAzBQ,eAAA;G/B4gIP;E+B3gIO;;IAEE,eAAA;IACA,8BAAA;G/B6gIT;E+BzgIO;;;IAGE,eAAA;IACA,0BAAA;G/B2gIT;E+BvgIO;;;IAGE,eAAA;IACA,8BAAA;G/BygIT;CACF;A+B3mID;EA8GI,eAAA;C/BggIH;A+B//HG;EACE,eAAA;C/BigIL;A+BjnID;EAqHI,eAAA;C/B+/HH;A+B9/HG;;EAEE,eAAA;C/BggIL;A+B5/HK;;;;EAEE,eAAA;C/BggIP;A+Bx/HD;EACE,0BAAA;EACA,sBAAA;C/B0/HD;A+B5/HD;EAKI,eAAA;C/B0/HH;A+Bz/HG;;EAEE,eAAA;EACA,8BAAA;C/B2/HL;A+BpgID;EAcI,eAAA;C/By/HH;A+BvgID;EAmBM,eAAA;C/Bu/HL;A+Br/HK;;EAEE,eAAA;EACA,8BAAA;C/Bu/HP;A+Bn/HK;;;EAGE,eAAA;EACA,0BAAA;C/Bq/HP;A+Bj/HK;;;EAGE,eAAA;EACA,8BAAA;C/Bm/HP;A+B3hID;EA+CI,sBAAA;C/B++HH;A+B9+HG;;EAEE,0BAAA;C/Bg/HL;A+BliID;EAqDM,0BAAA;C/Bg/HL;A+BriID;;EA2DI,sBAAA;C/B8+HH;A+Bx+HK;;;EAGE,0BAAA;EACA,eAAA;C/B0+HP;A+Bn8HC;EAAA;IA/BQ,sBAAA;G/Bs+HP;E+Bv8HD;IA5BQ,0BAAA;G/Bs+HP;E+B18HD;IAzBQ,eAAA;G/Bs+HP;E+Br+HO;;IAEE,eAAA;IACA,8BAAA;G/Bu+HT;E+Bn+HO;;;IAGE,eAAA;IACA,0BAAA;G/Bq+HT;E+Bj+HO;;;IAGE,eAAA;IACA,8BAAA;G/Bm+HT;CACF;A+B3kID;EA+GI,eAAA;C/B+9HH;A+B99HG;EACE,eAAA;C/Bg+HL;A+BjlID;EAsHI,eAAA;C/B89HH;A+B79HG;;EAEE,eAAA;C/B+9HL;A+B39HK;;;;EAEE,eAAA;C/B+9HP;AkCzmJD;EACE,kBAAA;EACA,oBAAA;EACA,iBAAA;EACA,0BAAA;EACA,mBAAA;ClC2mJD;AkChnJD;EAQI,sBAAA;ClC2mJH;AkCnnJD;EAWM,kBAAA;EACA,eAAA;EACA,eAAA;ClC2mJL;AkCxnJD;EAkBI,eAAA;ClCymJH;AmC7nJD;EACE,sBAAA;EACA,gBAAA;EACA,eAAA;EACA,mBAAA;CnC+nJD;AmCnoJD;EAOI,gBAAA;CnC+nJH;AmCtoJD;;EAUM,mBAAA;EACA,YAAA;EACA,kBAAA;EACA,wBAAA;EACA,sBAAA;EACA,eAAA;EACA,0BAAA;EACA,0BAAA;EACA,kBAAA;CnCgoJL;AmC9nJG;;EAGI,eAAA;EPXN,+BAAA;EACG,4BAAA;C5B2oJJ;AmC7nJG;;EPvBF,gCAAA;EACG,6BAAA;C5BwpJJ;AmCxnJG;;;;EAEE,WAAA;EACA,eAAA;EACA,0BAAA;EACA,sBAAA;CnC4nJL;AmCtnJG;;;;;;EAGE,WAAA;EACA,eAAA;EACA,0BAAA;EACA,sBAAA;EACA,gBAAA;CnC2nJL;AmClrJD;;;;;;EAkEM,eAAA;EACA,0BAAA;EACA,sBAAA;EACA,oBAAA;CnCwnJL;AmC/mJD;;EC3EM,mBAAA;EACA,gBAAA;EACA,uBAAA;CpC8rJL;AoC5rJG;;ERKF,+BAAA;EACG,4BAAA;C5B2rJJ;AoC3rJG;;ERTF,gCAAA;EACG,6BAAA;C5BwsJJ;AmC1nJD;;EChFM,kBAAA;EACA,gBAAA;EACA,iBAAA;CpC8sJL;AoC5sJG;;ERKF,+BAAA;EACG,4BAAA;C5B2sJJ;AoC3sJG;;ERTF,gCAAA;EACG,6BAAA;C5BwtJJ;AqC3tJD;EACE,gBAAA;EACA,eAAA;EACA,iBAAA;EACA,mBAAA;CrC6tJD;AqCjuJD;EAOI,gBAAA;CrC6tJH;AqCpuJD;;EAUM,sBAAA;EACA,kBAAA;EACA,0BAAA;EACA,0BAAA;EACA,oBAAA;CrC8tJL;AqC5uJD;;EAmBM,sBAAA;EACA,0BAAA;CrC6tJL;AqCjvJD;;EA2BM,aAAA;CrC0tJL;AqCrvJD;;EAkCM,YAAA;CrCutJL;AqCzvJD;;;;EA2CM,eAAA;EACA,0BAAA;EACA,oBAAA;CrCotJL;AsClwJD;EACE,gBAAA;EACA,wBAAA;EACA,eAAA;EACA,kBAAA;EACA,eAAA;EACA,eAAA;EACA,mBAAA;EACA,oBAAA;EACA,yBAAA;EACA,qBAAA;CtCowJD;AsChwJG;;EAEE,eAAA;EACA,sBAAA;EACA,gBAAA;CtCkwJL;AsC7vJC;EACE,cAAA;CtC+vJH;AsC3vJC;EACE,mBAAA;EACA,UAAA;CtC6vJH;AsCtvJD;ECtCE,0BAAA;CvC+xJD;AuC5xJG;;EAEE,0BAAA;CvC8xJL;AsCzvJD;EC1CE,0BAAA;CvCsyJD;AuCnyJG;;EAEE,0BAAA;CvCqyJL;AsC5vJD;EC9CE,0BAAA;CvC6yJD;AuC1yJG;;EAEE,0BAAA;CvC4yJL;AsC/vJD;EClDE,0BAAA;CvCozJD;AuCjzJG;;EAEE,0BAAA;CvCmzJL;AsClwJD;ECtDE,0BAAA;CvC2zJD;AuCxzJG;;EAEE,0BAAA;CvC0zJL;AsCrwJD;EC1DE,0BAAA;CvCk0JD;AuC/zJG;;EAEE,0BAAA;CvCi0JL;AwCn0JD;EACE,sBAAA;EACA,gBAAA;EACA,iBAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,eAAA;EACA,uBAAA;EACA,oBAAA;EACA,mBAAA;EACA,0BAAA;EACA,oBAAA;CxCq0JD;AwCl0JC;EACE,cAAA;CxCo0JH;AwCh0JC;EACE,mBAAA;EACA,UAAA;CxCk0JH;AwC/zJC;;EAEE,OAAA;EACA,iBAAA;CxCi0JH;AwC5zJG;;EAEE,eAAA;EACA,sBAAA;EACA,gBAAA;CxC8zJL;AwCzzJC;;EAEE,eAAA;EACA,0BAAA;CxC2zJH;AwCxzJC;EACE,aAAA;CxC0zJH;AwCvzJC;EACE,kBAAA;CxCyzJH;AwCtzJC;EACE,iBAAA;CxCwzJH;AyCl3JD;EACE,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,eAAA;EACA,0BAAA;CzCo3JD;AyCz3JD;;EASI,eAAA;CzCo3JH;AyC73JD;EAaI,oBAAA;EACA,gBAAA;EACA,iBAAA;CzCm3JH;AyCl4JD;EAmBI,0BAAA;CzCk3JH;AyC/2JC;;EAEE,mBAAA;CzCi3JH;AyCz4JD;EA4BI,gBAAA;CzCg3JH;AyC91JD;EAAA;IAdI,kBAAA;IACA,qBAAA;GzCg3JD;EyC92JC;;IAEE,mBAAA;IACA,oBAAA;GzCg3JH;EyCx2JH;;IAHM,gBAAA;GzC+2JH;CACF;A0C15JD;EACE,eAAA;EACA,aAAA;EACA,oBAAA;EACA,wBAAA;EACA,0BAAA;EACA,0BAAA;EACA,mBAAA;ErCiLA,4CAAA;EACK,uCAAA;EACG,oCAAA;CL4uJT;A0Ct6JD;;EAaI,kBAAA;EACA,mBAAA;C1C65JH;A0Cz5JC;;;EAGE,sBAAA;C1C25JH;A0Ch7JD;EA0BI,aAAA;EACA,eAAA;C1Cy5JH;A2Cl7JD;EACE,cAAA;EACA,oBAAA;EACA,8BAAA;EACA,mBAAA;C3Co7JD;A2Cx7JD;EAQI,cAAA;EAEA,eAAA;C3Ck7JH;A2C57JD;EAeI,kBAAA;C3Cg7JH;A2C/7JD;;EAqBI,iBAAA;C3C86JH;A2Cn8JD;EAyBI,gBAAA;C3C66JH;A2Cr6JD;;EAEE,oBAAA;C3Cu6JD;A2Cz6JD;;EAMI,mBAAA;EACA,UAAA;EACA,aAAA;EACA,eAAA;C3Cu6JH;A2C/5JD;ECvDE,0BAAA;EACA,sBAAA;EACA,eAAA;C5Cy9JD;A2Cp6JD;EClDI,0BAAA;C5Cy9JH;A2Cv6JD;EC/CI,eAAA;C5Cy9JH;A2Ct6JD;EC3DE,0BAAA;EACA,sBAAA;EACA,eAAA;C5Co+JD;A2C36JD;ECtDI,0BAAA;C5Co+JH;A2C96JD;ECnDI,eAAA;C5Co+JH;A2C76JD;EC/DE,0BAAA;EACA,sBAAA;EACA,eAAA;C5C++JD;A2Cl7JD;EC1DI,0BAAA;C5C++JH;A2Cr7JD;ECvDI,eAAA;C5C++JH;A2Cp7JD;ECnEE,0BAAA;EACA,sBAAA;EACA,eAAA;C5C0/JD;A2Cz7JD;EC9DI,0BAAA;C5C0/JH;A2C57JD;EC3DI,eAAA;C5C0/JH;A6C5/JD;EACE;IAAQ,4BAAA;G7C+/JP;E6C9/JD;IAAQ,yBAAA;G7CigKP;CACF;A6C9/JD;EACE;IAAQ,4BAAA;G7CigKP;E6ChgKD;IAAQ,yBAAA;G7CmgKP;CACF;A6CtgKD;EACE;IAAQ,4BAAA;G7CigKP;E6ChgKD;IAAQ,yBAAA;G7CmgKP;CACF;A6C5/JD;EACE,iBAAA;EACA,aAAA;EACA,oBAAA;EACA,0BAAA;EACA,mBAAA;ExCsCA,uDAAA;EACQ,+CAAA;CLy9JT;A6C3/JD;EACE,YAAA;EACA,UAAA;EACA,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,mBAAA;EACA,0BAAA;ExCyBA,uDAAA;EACQ,+CAAA;EAyHR,oCAAA;EACK,+BAAA;EACG,4BAAA;CL62JT;A6Cx/JD;;ECCI,8MAAA;EACA,yMAAA;EACA,sMAAA;EDAF,mCAAA;UAAA,2BAAA;C7C4/JD;A6Cr/JD;;ExC5CE,2DAAA;EACK,sDAAA;EACG,mDAAA;CLqiKT;A6Cl/JD;EErEE,0BAAA;C/C0jKD;A+CvjKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9C0gKH;A6Ct/JD;EEzEE,0BAAA;C/CkkKD;A+C/jKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9CkhKH;A6C1/JD;EE7EE,0BAAA;C/C0kKD;A+CvkKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9C0hKH;A6C9/JD;EEjFE,0BAAA;C/CklKD;A+C/kKC;EDgDE,8MAAA;EACA,yMAAA;EACA,sMAAA;C9CkiKH;AgD1lKD;EAEE,iBAAA;ChD2lKD;AgDzlKC;EACE,cAAA;ChD2lKH;AgDvlKD;;EAEE,QAAA;EACA,iBAAA;ChDylKD;AgDtlKD;EACE,eAAA;ChDwlKD;AgDrlKD;EACE,eAAA;ChDulKD;AgDplKC;EACE,gBAAA;ChDslKH;AgDllKD;;EAEE,mBAAA;ChDolKD;AgDjlKD;;EAEE,oBAAA;ChDmlKD;AgDhlKD;;;EAGE,oBAAA;EACA,oBAAA;ChDklKD;AgD/kKD;EACE,uBAAA;ChDilKD;AgD9kKD;EACE,uBAAA;ChDglKD;AgD5kKD;EACE,cAAA;EACA,mBAAA;ChD8kKD;AgDxkKD;EACE,gBAAA;EACA,iBAAA;ChD0kKD;AiDjoKD;EAEE,oBAAA;EACA,gBAAA;CjDkoKD;AiD1nKD;EACE,mBAAA;EACA,eAAA;EACA,mBAAA;EAEA,oBAAA;EACA,0BAAA;EACA,0BAAA;CjD2nKD;AiDxnKC;ErB3BA,6BAAA;EACC,4BAAA;C5BspKF;AiDznKC;EACE,iBAAA;ErBvBF,gCAAA;EACC,+BAAA;C5BmpKF;AiDlnKD;;EAEE,eAAA;CjDonKD;AiDtnKD;;EAKI,eAAA;CjDqnKH;AiDjnKC;;;;EAEE,sBAAA;EACA,eAAA;EACA,0BAAA;CjDqnKH;AiDjnKD;EACE,YAAA;EACA,iBAAA;CjDmnKD;AiD9mKC;;;EAGE,0BAAA;EACA,eAAA;EACA,oBAAA;CjDgnKH;AiDrnKC;;;EASI,eAAA;CjDinKL;AiD1nKC;;;EAYI,eAAA;CjDmnKL;AiD9mKC;;;EAGE,WAAA;EACA,eAAA;EACA,0BAAA;EACA,sBAAA;CjDgnKH;AiDtnKC;;;;;;;;;EAYI,eAAA;CjDqnKL;AiDjoKC;;;EAeI,eAAA;CjDunKL;AkDztKC;EACE,eAAA;EACA,0BAAA;ClD2tKH;AkDztKG;;EAEE,eAAA;ClD2tKL;AkD7tKG;;EAKI,eAAA;ClD4tKP;AkDztKK;;;;EAEE,eAAA;EACA,0BAAA;ClD6tKP;AkD3tKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClDguKP;AkDtvKC;EACE,eAAA;EACA,0BAAA;ClDwvKH;AkDtvKG;;EAEE,eAAA;ClDwvKL;AkD1vKG;;EAKI,eAAA;ClDyvKP;AkDtvKK;;;;EAEE,eAAA;EACA,0BAAA;ClD0vKP;AkDxvKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClD6vKP;AkDnxKC;EACE,eAAA;EACA,0BAAA;ClDqxKH;AkDnxKG;;EAEE,eAAA;ClDqxKL;AkDvxKG;;EAKI,eAAA;ClDsxKP;AkDnxKK;;;;EAEE,eAAA;EACA,0BAAA;ClDuxKP;AkDrxKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClD0xKP;AkDhzKC;EACE,eAAA;EACA,0BAAA;ClDkzKH;AkDhzKG;;EAEE,eAAA;ClDkzKL;AkDpzKG;;EAKI,eAAA;ClDmzKP;AkDhzKK;;;;EAEE,eAAA;EACA,0BAAA;ClDozKP;AkDlzKK;;;;;;EAGE,YAAA;EACA,0BAAA;EACA,sBAAA;ClDuzKP;AiDttKD;EACE,cAAA;EACA,mBAAA;CjDwtKD;AiDttKD;EACE,iBAAA;EACA,iBAAA;CjDwtKD;AmDl1KD;EACE,oBAAA;EACA,0BAAA;EACA,8BAAA;EACA,mBAAA;E9C0DA,kDAAA;EACQ,0CAAA;CL2xKT;AmDj1KD;EACE,cAAA;CnDm1KD;AmD90KD;EACE,mBAAA;EACA,qCAAA;EvBpBA,6BAAA;EACC,4BAAA;C5Bq2KF;AmDp1KD;EAMI,eAAA;CnDi1KH;AmD50KD;EACE,cAAA;EACA,iBAAA;EACA,gBAAA;EACA,eAAA;CnD80KD;AmDl1KD;;;;;EAWI,eAAA;CnD80KH;AmDz0KD;EACE,mBAAA;EACA,0BAAA;EACA,8BAAA;EvBxCA,gCAAA;EACC,+BAAA;C5Bo3KF;AmDn0KD;;EAGI,iBAAA;CnDo0KH;AmDv0KD;;EAMM,oBAAA;EACA,iBAAA;CnDq0KL;AmDj0KG;;EAEI,cAAA;EvBvEN,6BAAA;EACC,4BAAA;C5B24KF;AmD/zKG;;EAEI,iBAAA;EvBvEN,gCAAA;EACC,+BAAA;C5By4KF;AmDx1KD;EvB1DE,2BAAA;EACC,0BAAA;C5Bq5KF;AmD3zKD;EAEI,oBAAA;CnD4zKH;AmDzzKD;EACE,oBAAA;CnD2zKD;AmDnzKD;;;EAII,iBAAA;CnDozKH;AmDxzKD;;;EAOM,mBAAA;EACA,oBAAA;CnDszKL;AmD9zKD;;EvBzGE,6BAAA;EACC,4BAAA;C5B26KF;AmDn0KD;;;;EAmBQ,4BAAA;EACA,6BAAA;CnDszKP;AmD10KD;;;;;;;;EAwBU,4BAAA;CnD4zKT;AmDp1KD;;;;;;;;EA4BU,6BAAA;CnDk0KT;AmD91KD;;EvBjGE,gCAAA;EACC,+BAAA;C5Bm8KF;AmDn2KD;;;;EAyCQ,+BAAA;EACA,gCAAA;CnDg0KP;AmD12KD;;;;;;;;EA8CU,+BAAA;CnDs0KT;AmDp3KD;;;;;;;;EAkDU,gCAAA;CnD40KT;AmD93KD;;;;EA2DI,8BAAA;CnDy0KH;AmDp4KD;;EA+DI,cAAA;CnDy0KH;AmDx4KD;;EAmEI,UAAA;CnDy0KH;AmD54KD;;;;;;;;;;;;EA0EU,eAAA;CnDg1KT;AmD15KD;;;;;;;;;;;;EA8EU,gBAAA;CnD01KT;AmDx6KD;;;;;;;;EAuFU,iBAAA;CnD21KT;AmDl7KD;;;;;;;;EAgGU,iBAAA;CnD41KT;AmD57KD;EAsGI,UAAA;EACA,iBAAA;CnDy1KH;AmD/0KD;EACE,oBAAA;CnDi1KD;AmDl1KD;EAKI,iBAAA;EACA,mBAAA;CnDg1KH;AmDt1KD;EASM,gBAAA;CnDg1KL;AmDz1KD;EAcI,iBAAA;CnD80KH;AmD51KD;;EAkBM,8BAAA;CnD80KL;AmDh2KD;EAuBI,cAAA;CnD40KH;AmDn2KD;EAyBM,iCAAA;CnD60KL;AmDt0KD;EC1PE,sBAAA;CpDmkLD;AoDjkLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDmkLH;AoDtkLC;EAMI,0BAAA;CpDmkLL;AoDzkLC;EASI,eAAA;EACA,0BAAA;CpDmkLL;AoDhkLC;EAEI,6BAAA;CpDikLL;AmDr1KD;EC7PE,sBAAA;CpDqlLD;AoDnlLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDqlLH;AoDxlLC;EAMI,0BAAA;CpDqlLL;AoD3lLC;EASI,eAAA;EACA,0BAAA;CpDqlLL;AoDllLC;EAEI,6BAAA;CpDmlLL;AmDp2KD;EChQE,sBAAA;CpDumLD;AoDrmLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDumLH;AoD1mLC;EAMI,0BAAA;CpDumLL;AoD7mLC;EASI,eAAA;EACA,0BAAA;CpDumLL;AoDpmLC;EAEI,6BAAA;CpDqmLL;AmDn3KD;ECnQE,sBAAA;CpDynLD;AoDvnLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpDynLH;AoD5nLC;EAMI,0BAAA;CpDynLL;AoD/nLC;EASI,eAAA;EACA,0BAAA;CpDynLL;AoDtnLC;EAEI,6BAAA;CpDunLL;AmDl4KD;ECtQE,sBAAA;CpD2oLD;AoDzoLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpD2oLH;AoD9oLC;EAMI,0BAAA;CpD2oLL;AoDjpLC;EASI,eAAA;EACA,0BAAA;CpD2oLL;AoDxoLC;EAEI,6BAAA;CpDyoLL;AmDj5KD;ECzQE,sBAAA;CpD6pLD;AoD3pLC;EACE,eAAA;EACA,0BAAA;EACA,sBAAA;CpD6pLH;AoDhqLC;EAMI,0BAAA;CpD6pLL;AoDnqLC;EASI,eAAA;EACA,0BAAA;CpD6pLL;AoD1pLC;EAEI,6BAAA;CpD2pLL;AqD3qLD;EACE,mBAAA;EACA,eAAA;EACA,UAAA;EACA,WAAA;EACA,iBAAA;CrD6qLD;AqDlrLD;;;;;EAYI,mBAAA;EACA,OAAA;EACA,QAAA;EACA,UAAA;EACA,aAAA;EACA,YAAA;EACA,UAAA;CrD6qLH;AqDxqLD;EACE,uBAAA;CrD0qLD;AqDtqLD;EACE,oBAAA;CrDwqLD;AsDnsLD;EACE,iBAAA;EACA,cAAA;EACA,oBAAA;EACA,0BAAA;EACA,0BAAA;EACA,mBAAA;EjDwDA,wDAAA;EACQ,gDAAA;CL8oLT;AsD7sLD;EASI,mBAAA;EACA,kCAAA;CtDusLH;AsDlsLD;EACE,cAAA;EACA,mBAAA;CtDosLD;AsDlsLD;EACE,aAAA;EACA,mBAAA;CtDosLD;AuD1tLD;EACE,aAAA;EACA,gBAAA;EACA,kBAAA;EACA,eAAA;EACA,eAAA;EACA,6BAAA;EjCRA,aAAA;EAGA,0BAAA;CtBmuLD;AuD3tLC;;EAEE,eAAA;EACA,sBAAA;EACA,gBAAA;EjCfF,aAAA;EAGA,0BAAA;CtB2uLD;AuDvtLC;EACE,WAAA;EACA,gBAAA;EACA,wBAAA;EACA,UAAA;EACA,yBAAA;CvDytLH;AwD9uLD;EACE,iBAAA;CxDgvLD;AwD5uLD;EACE,cAAA;EACA,iBAAA;EACA,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,kCAAA;EAIA,WAAA;CxD2uLD;AwDxuLC;EnD+GA,sCAAA;EACI,kCAAA;EACC,iCAAA;EACG,8BAAA;EAkER,oDAAA;EAEK,0CAAA;EACG,oCAAA;CL2jLT;AwD9uLC;EnD2GA,mCAAA;EACI,+BAAA;EACC,8BAAA;EACG,2BAAA;CLsoLT;AwDlvLD;EACE,mBAAA;EACA,iBAAA;CxDovLD;AwDhvLD;EACE,mBAAA;EACA,YAAA;EACA,aAAA;CxDkvLD;AwD9uLD;EACE,mBAAA;EACA,0BAAA;EACA,0BAAA;EACA,qCAAA;EACA,mBAAA;EnDaA,iDAAA;EACQ,yCAAA;EmDZR,qCAAA;UAAA,6BAAA;EAEA,WAAA;CxDgvLD;AwD5uLD;EACE,gBAAA;EACA,OAAA;EACA,SAAA;EACA,UAAA;EACA,QAAA;EACA,cAAA;EACA,0BAAA;CxD8uLD;AwD5uLC;ElCrEA,WAAA;EAGA,yBAAA;CtBkzLD;AwD/uLC;ElCtEA,aAAA;EAGA,0BAAA;CtBszLD;AwD9uLD;EACE,cAAA;EACA,iCAAA;EACA,0BAAA;CxDgvLD;AwD7uLD;EACE,iBAAA;CxD+uLD;AwD3uLD;EACE,UAAA;EACA,wBAAA;CxD6uLD;AwDxuLD;EACE,mBAAA;EACA,cAAA;CxD0uLD;AwDtuLD;EACE,cAAA;EACA,kBAAA;EACA,8BAAA;CxDwuLD;AwD3uLD;EAQI,iBAAA;EACA,iBAAA;CxDsuLH;AwD/uLD;EAaI,kBAAA;CxDquLH;AwDlvLD;EAiBI,eAAA;CxDouLH;AwD/tLD;EACE,mBAAA;EACA,aAAA;EACA,YAAA;EACA,aAAA;EACA,iBAAA;CxDiuLD;AwD/sLD;EAZE;IACE,aAAA;IACA,kBAAA;GxD8tLD;EwD5tLD;InDvEA,kDAAA;IACQ,0CAAA;GLsyLP;EwD3tLD;IAAY,aAAA;GxD8tLX;CACF;AwDztLD;EAFE;IAAY,aAAA;GxD+tLX;CACF;AyD92LD;EACE,mBAAA;EACA,cAAA;EACA,eAAA;ECRA,4DAAA;EAEA,mBAAA;EACA,oBAAA;EACA,uBAAA;EACA,iBAAA;EACA,wBAAA;EACA,iBAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mBAAA;EACA,qBAAA;EACA,kBAAA;EDHA,gBAAA;EnCVA,WAAA;EAGA,yBAAA;CtBq4LD;AyD13LC;EnCdA,aAAA;EAGA,0BAAA;CtBy4LD;AyD73LC;EAAW,iBAAA;EAAmB,eAAA;CzDi4L/B;AyDh4LC;EAAW,iBAAA;EAAmB,eAAA;CzDo4L/B;AyDn4LC;EAAW,gBAAA;EAAmB,eAAA;CzDu4L/B;AyDt4LC;EAAW,kBAAA;EAAmB,eAAA;CzD04L/B;AyDt4LD;EACE,iBAAA;EACA,iBAAA;EACA,eAAA;EACA,mBAAA;EACA,0BAAA;EACA,mBAAA;CzDw4LD;AyDp4LD;EACE,mBAAA;EACA,SAAA;EACA,UAAA;EACA,0BAAA;EACA,oBAAA;CzDs4LD;AyDl4LC;EACE,UAAA;EACA,UAAA;EACA,kBAAA;EACA,wBAAA;EACA,0BAAA;CzDo4LH;AyDl4LC;EACE,UAAA;EACA,WAAA;EACA,oBAAA;EACA,wBAAA;EACA,0BAAA;CzDo4LH;AyDl4LC;EACE,UAAA;EACA,UAAA;EACA,oBAAA;EACA,wBAAA;EACA,0BAAA;CzDo4LH;AyDl4LC;EACE,SAAA;EACA,QAAA;EACA,iBAAA;EACA,4BAAA;EACA,4BAAA;CzDo4LH;AyDl4LC;EACE,SAAA;EACA,SAAA;EACA,iBAAA;EACA,4BAAA;EACA,2BAAA;CzDo4LH;AyDl4LC;EACE,OAAA;EACA,UAAA;EACA,kBAAA;EACA,wBAAA;EACA,6BAAA;CzDo4LH;AyDl4LC;EACE,OAAA;EACA,WAAA;EACA,iBAAA;EACA,wBAAA;EACA,6BAAA;CzDo4LH;AyDl4LC;EACE,OAAA;EACA,UAAA;EACA,iBAAA;EACA,wBAAA;EACA,6BAAA;CzDo4LH;A2Dj+LD;EACE,mBAAA;EACA,OAAA;EACA,QAAA;EACA,cAAA;EACA,cAAA;EACA,iBAAA;EACA,aAAA;EDXA,4DAAA;EAEA,mBAAA;EACA,oBAAA;EACA,uBAAA;EACA,iBAAA;EACA,wBAAA;EACA,iBAAA;EACA,kBAAA;EACA,sBAAA;EACA,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mBAAA;EACA,qBAAA;EACA,kBAAA;ECAA,gBAAA;EAEA,0BAAA;EACA,qCAAA;UAAA,6BAAA;EACA,0BAAA;EACA,qCAAA;EACA,mBAAA;EtD8CA,kDAAA;EACQ,0CAAA;CLi8LT;A2D5+LC;EAAY,kBAAA;C3D++Lb;A2D9+LC;EAAY,kBAAA;C3Di/Lb;A2Dh/LC;EAAY,iBAAA;C3Dm/Lb;A2Dl/LC;EAAY,mBAAA;C3Dq/Lb;A2Dl/LD;EACE,UAAA;EACA,kBAAA;EACA,gBAAA;EACA,0BAAA;EACA,iCAAA;EACA,2BAAA;C3Do/LD;A2Dj/LD;EACE,kBAAA;C3Dm/LD;A2D3+LC;;EAEE,mBAAA;EACA,eAAA;EACA,SAAA;EACA,UAAA;EACA,0BAAA;EACA,oBAAA;C3D6+LH;A2D1+LD;EACE,mBAAA;C3D4+LD;A2D1+LD;EACE,mBAAA;EACA,YAAA;C3D4+LD;A2Dx+LC;EACE,UAAA;EACA,mBAAA;EACA,uBAAA;EACA,0BAAA;EACA,sCAAA;EACA,cAAA;C3D0+LH;A2Dz+LG;EACE,aAAA;EACA,YAAA;EACA,mBAAA;EACA,uBAAA;EACA,0BAAA;C3D2+LL;A2Dx+LC;EACE,SAAA;EACA,YAAA;EACA,kBAAA;EACA,qBAAA;EACA,4BAAA;EACA,wCAAA;C3D0+LH;A2Dz+LG;EACE,aAAA;EACA,UAAA;EACA,cAAA;EACA,qBAAA;EACA,4BAAA;C3D2+LL;A2Dx+LC;EACE,UAAA;EACA,mBAAA;EACA,oBAAA;EACA,6BAAA;EACA,yCAAA;EACA,WAAA;C3D0+LH;A2Dz+LG;EACE,aAAA;EACA,SAAA;EACA,mBAAA;EACA,oBAAA;EACA,6BAAA;C3D2+LL;A2Dv+LC;EACE,SAAA;EACA,aAAA;EACA,kBAAA;EACA,sBAAA;EACA,2BAAA;EACA,uCAAA;C3Dy+LH;A2Dx+LG;EACE,aAAA;EACA,WAAA;EACA,sBAAA;EACA,2BAAA;EACA,cAAA;C3D0+LL;A4DnmMD;EACE,mBAAA;C5DqmMD;A4DlmMD;EACE,mBAAA;EACA,iBAAA;EACA,YAAA;C5DomMD;A4DvmMD;EAMI,cAAA;EACA,mBAAA;EvD6KF,0CAAA;EACK,qCAAA;EACG,kCAAA;CLw7LT;A4D9mMD;;EAcM,eAAA;C5DomML;A4D1kMC;EAAA;IvDiKA,uDAAA;IAEK,6CAAA;IACG,uCAAA;IA7JR,oCAAA;IAEQ,4BAAA;IA+GR,4BAAA;IAEQ,oBAAA;GL69LP;E4DxmMG;;IvDmHJ,2CAAA;IACQ,mCAAA;IuDjHF,QAAA;G5D2mML;E4DzmMG;;IvD8GJ,4CAAA;IACQ,oCAAA;IuD5GF,QAAA;G5D4mML;E4D1mMG;;;IvDyGJ,wCAAA;IACQ,gCAAA;IuDtGF,QAAA;G5D6mML;CACF;A4DnpMD;;;EA6CI,eAAA;C5D2mMH;A4DxpMD;EAiDI,QAAA;C5D0mMH;A4D3pMD;;EAsDI,mBAAA;EACA,OAAA;EACA,YAAA;C5DymMH;A4DjqMD;EA4DI,WAAA;C5DwmMH;A4DpqMD;EA+DI,YAAA;C5DwmMH;A4DvqMD;;EAmEI,QAAA;C5DwmMH;A4D3qMD;EAuEI,YAAA;C5DumMH;A4D9qMD;EA0EI,WAAA;C5DumMH;A4D/lMD;EACE,mBAAA;EACA,OAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EtC9FA,aAAA;EAGA,0BAAA;EsC6FA,gBAAA;EACA,eAAA;EACA,mBAAA;EACA,0CAAA;C5DkmMD;A4D7lMC;EdlGE,mGAAA;EACA,8FAAA;EACA,qHAAA;EAAA,+FAAA;EACA,4BAAA;EACA,uHAAA;C9CksMH;A4DjmMC;EACE,WAAA;EACA,SAAA;EdvGA,mGAAA;EACA,8FAAA;EACA,qHAAA;EAAA,+FAAA;EACA,4BAAA;EACA,uHAAA;C9C2sMH;A4DnmMC;;EAEE,WAAA;EACA,eAAA;EACA,sBAAA;EtCtHF,aAAA;EAGA,0BAAA;CtB0tMD;A4DpoMD;;;;EAsCI,mBAAA;EACA,SAAA;EACA,kBAAA;EACA,WAAA;EACA,sBAAA;C5DomMH;A4D9oMD;;EA8CI,UAAA;EACA,mBAAA;C5DomMH;A4DnpMD;;EAmDI,WAAA;EACA,oBAAA;C5DomMH;A4DxpMD;;EAwDI,YAAA;EACA,aAAA;EACA,eAAA;EACA,mBAAA;C5DomMH;A4D/lMG;EACE,iBAAA;C5DimML;A4D7lMG;EACE,iBAAA;C5D+lML;A4DrlMD;EACE,mBAAA;EACA,aAAA;EACA,UAAA;EACA,YAAA;EACA,WAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,mBAAA;C5DulMD;A4DhmMD;EAYI,sBAAA;EACA,YAAA;EACA,aAAA;EACA,YAAA;EACA,oBAAA;EACA,0BAAA;EACA,oBAAA;EACA,gBAAA;EAWA,0BAAA;EACA,mCAAA;C5D6kMH;A4D5mMD;EAkCI,UAAA;EACA,YAAA;EACA,aAAA;EACA,0BAAA;C5D6kMH;A4DtkMD;EACE,mBAAA;EACA,UAAA;EACA,WAAA;EACA,aAAA;EACA,YAAA;EACA,kBAAA;EACA,qBAAA;EACA,eAAA;EACA,mBAAA;EACA,0CAAA;C5DwkMD;A4DvkMC;EACE,kBAAA;C5DykMH;A4DhiMD;EAhCE;;;;IAKI,YAAA;IACA,aAAA;IACA,kBAAA;IACA,gBAAA;G5DkkMH;E4D1kMD;;IAYI,mBAAA;G5DkkMH;E4D9kMD;;IAgBI,oBAAA;G5DkkMH;E4D7jMD;IACE,UAAA;IACA,WAAA;IACA,qBAAA;G5D+jMD;E4D3jMD;IACE,aAAA;G5D6jMD;CACF;A6D3zMC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEE,aAAA;EACA,eAAA;C7Dy1MH;A6Dv1MC;;;;;;;;;;;;;;;EACE,YAAA;C7Du2MH;AiC/2MD;E6BRE,eAAA;EACA,kBAAA;EACA,mBAAA;C9D03MD;AiCj3MD;EACE,wBAAA;CjCm3MD;AiCj3MD;EACE,uBAAA;CjCm3MD;AiC32MD;EACE,yBAAA;CjC62MD;AiC32MD;EACE,0BAAA;CjC62MD;AiC32MD;EACE,mBAAA;CjC62MD;AiC32MD;E8BzBE,YAAA;EACA,mBAAA;EACA,kBAAA;EACA,8BAAA;EACA,UAAA;C/Du4MD;AiCz2MD;EACE,yBAAA;CjC22MD;AiCp2MD;EACE,gBAAA;CjCs2MD;AgEv4MD;EACE,oBAAA;ChEy4MD;AgEn4MD;;;;ECdE,yBAAA;CjEu5MD;AgEl4MD;;;;;;;;;;;;EAYE,yBAAA;ChEo4MD;AgE73MD;EAAA;IChDE,0BAAA;GjEi7MC;EiEh7MD;IAAU,0BAAA;GjEm7MT;EiEl7MD;IAAU,8BAAA;GjEq7MT;EiEp7MD;;IACU,+BAAA;GjEu7MT;CACF;AgEv4MD;EAAA;IAFI,0BAAA;GhE64MD;CACF;AgEv4MD;EAAA;IAFI,2BAAA;GhE64MD;CACF;AgEv4MD;EAAA;IAFI,iCAAA;GhE64MD;CACF;AgEt4MD;EAAA;ICrEE,0BAAA;GjE+8MC;EiE98MD;IAAU,0BAAA;GjEi9MT;EiEh9MD;IAAU,8BAAA;GjEm9MT;EiEl9MD;;IACU,+BAAA;GjEq9MT;CACF;AgEh5MD;EAAA;IAFI,0BAAA;GhEs5MD;CACF;AgEh5MD;EAAA;IAFI,2BAAA;GhEs5MD;CACF;AgEh5MD;EAAA;IAFI,iCAAA;GhEs5MD;CACF;AgE/4MD;EAAA;IC1FE,0BAAA;GjE6+MC;EiE5+MD;IAAU,0BAAA;GjE++MT;EiE9+MD;IAAU,8BAAA;GjEi/MT;EiEh/MD;;IACU,+BAAA;GjEm/MT;CACF;AgEz5MD;EAAA;IAFI,0BAAA;GhE+5MD;CACF;AgEz5MD;EAAA;IAFI,2BAAA;GhE+5MD;CACF;AgEz5MD;EAAA;IAFI,iCAAA;GhE+5MD;CACF;AgEx5MD;EAAA;IC/GE,0BAAA;GjE2gNC;EiE1gND;IAAU,0BAAA;GjE6gNT;EiE5gND;IAAU,8BAAA;GjE+gNT;EiE9gND;;IACU,+BAAA;GjEihNT;CACF;AgEl6MD;EAAA;IAFI,0BAAA;GhEw6MD;CACF;AgEl6MD;EAAA;IAFI,2BAAA;GhEw6MD;CACF;AgEl6MD;EAAA;IAFI,iCAAA;GhEw6MD;CACF;AgEj6MD;EAAA;IC5HE,yBAAA;GjEiiNC;CACF;AgEj6MD;EAAA;ICjIE,yBAAA;GjEsiNC;CACF;AgEj6MD;EAAA;ICtIE,yBAAA;GjE2iNC;CACF;AgEj6MD;EAAA;IC3IE,yBAAA;GjEgjNC;CACF;AgE95MD;ECnJE,yBAAA;CjEojND;AgE35MD;EAAA;ICjKE,0BAAA;GjEgkNC;EiE/jND;IAAU,0BAAA;GjEkkNT;EiEjkND;IAAU,8BAAA;GjEokNT;EiEnkND;;IACU,+BAAA;GjEskNT;CACF;AgEz6MD;EACE,yBAAA;ChE26MD;AgEt6MD;EAAA;IAFI,0BAAA;GhE46MD;CACF;AgE16MD;EACE,yBAAA;ChE46MD;AgEv6MD;EAAA;IAFI,2BAAA;GhE66MD;CACF;AgE36MD;EACE,yBAAA;ChE66MD;AgEx6MD;EAAA;IAFI,iCAAA;GhE86MD;CACF;AgEv6MD;EAAA;ICpLE,yBAAA;GjE+lNC;CACF","file":"bootstrap.css","sourcesContent":["/*!\n * Bootstrap v3.3.5 (http://getbootstrap.com)\n * Copyright 2011-2015 Twitter, Inc.\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n */\n/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: 1px dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important;\n box-shadow: none !important;\n text-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('../fonts/glyphicons-halflings-regular.eot');\n src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\2a\";\n}\n.glyphicon-plus:before {\n content: \"\\2b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n.glyphicon-cd:before {\n content: \"\\e201\";\n}\n.glyphicon-save-file:before {\n content: \"\\e202\";\n}\n.glyphicon-open-file:before {\n content: \"\\e203\";\n}\n.glyphicon-level-up:before {\n content: \"\\e204\";\n}\n.glyphicon-copy:before {\n content: \"\\e205\";\n}\n.glyphicon-paste:before {\n content: \"\\e206\";\n}\n.glyphicon-alert:before {\n content: \"\\e209\";\n}\n.glyphicon-equalizer:before {\n content: \"\\e210\";\n}\n.glyphicon-king:before {\n content: \"\\e211\";\n}\n.glyphicon-queen:before {\n content: \"\\e212\";\n}\n.glyphicon-pawn:before {\n content: \"\\e213\";\n}\n.glyphicon-bishop:before {\n content: \"\\e214\";\n}\n.glyphicon-knight:before {\n content: \"\\e215\";\n}\n.glyphicon-baby-formula:before {\n content: \"\\e216\";\n}\n.glyphicon-tent:before {\n content: \"\\26fa\";\n}\n.glyphicon-blackboard:before {\n content: \"\\e218\";\n}\n.glyphicon-bed:before {\n content: \"\\e219\";\n}\n.glyphicon-apple:before {\n content: \"\\f8ff\";\n}\n.glyphicon-erase:before {\n content: \"\\e221\";\n}\n.glyphicon-hourglass:before {\n content: \"\\231b\";\n}\n.glyphicon-lamp:before {\n content: \"\\e223\";\n}\n.glyphicon-duplicate:before {\n content: \"\\e224\";\n}\n.glyphicon-piggy-bank:before {\n content: \"\\e225\";\n}\n.glyphicon-scissors:before {\n content: \"\\e226\";\n}\n.glyphicon-bitcoin:before {\n content: \"\\e227\";\n}\n.glyphicon-btc:before {\n content: \"\\e227\";\n}\n.glyphicon-xbt:before {\n content: \"\\e227\";\n}\n.glyphicon-yen:before {\n content: \"\\00a5\";\n}\n.glyphicon-jpy:before {\n content: \"\\00a5\";\n}\n.glyphicon-ruble:before {\n content: \"\\20bd\";\n}\n.glyphicon-rub:before {\n content: \"\\20bd\";\n}\n.glyphicon-scale:before {\n content: \"\\e230\";\n}\n.glyphicon-ice-lolly:before {\n content: \"\\e231\";\n}\n.glyphicon-ice-lolly-tasted:before {\n content: \"\\e232\";\n}\n.glyphicon-education:before {\n content: \"\\e233\";\n}\n.glyphicon-option-horizontal:before {\n content: \"\\e234\";\n}\n.glyphicon-option-vertical:before {\n content: \"\\e235\";\n}\n.glyphicon-menu-hamburger:before {\n content: \"\\e236\";\n}\n.glyphicon-modal-window:before {\n content: \"\\e237\";\n}\n.glyphicon-oil:before {\n content: \"\\e238\";\n}\n.glyphicon-grain:before {\n content: \"\\e239\";\n}\n.glyphicon-sunglasses:before {\n content: \"\\e240\";\n}\n.glyphicon-text-size:before {\n content: \"\\e241\";\n}\n.glyphicon-text-color:before {\n content: \"\\e242\";\n}\n.glyphicon-text-background:before {\n content: \"\\e243\";\n}\n.glyphicon-object-align-top:before {\n content: \"\\e244\";\n}\n.glyphicon-object-align-bottom:before {\n content: \"\\e245\";\n}\n.glyphicon-object-align-horizontal:before {\n content: \"\\e246\";\n}\n.glyphicon-object-align-left:before {\n content: \"\\e247\";\n}\n.glyphicon-object-align-vertical:before {\n content: \"\\e248\";\n}\n.glyphicon-object-align-right:before {\n content: \"\\e249\";\n}\n.glyphicon-triangle-right:before {\n content: \"\\e250\";\n}\n.glyphicon-triangle-left:before {\n content: \"\\e251\";\n}\n.glyphicon-triangle-bottom:before {\n content: \"\\e252\";\n}\n.glyphicon-triangle-top:before {\n content: \"\\e253\";\n}\n.glyphicon-console:before {\n content: \"\\e254\";\n}\n.glyphicon-superscript:before {\n content: \"\\e255\";\n}\n.glyphicon-subscript:before {\n content: \"\\e256\";\n}\n.glyphicon-menu-left:before {\n content: \"\\e257\";\n}\n.glyphicon-menu-right:before {\n content: \"\\e258\";\n}\n.glyphicon-menu-down:before {\n content: \"\\e259\";\n}\n.glyphicon-menu-up:before {\n content: \"\\e260\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #ffffff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\n[role=\"button\"] {\n cursor: pointer;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: normal;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n background-color: #fcf8e3;\n padding: .2em;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover,\na.text-primary:focus {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover,\na.text-success:focus {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover,\na.text-info:focus {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover,\na.text-warning:focus {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover,\na.text-danger:focus {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover,\na.bg-primary:focus {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover,\na.bg-success:focus {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover,\na.bg-info:focus {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover,\na.bg-warning:focus {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover,\na.bg-danger:focus {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted #777777;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: '\\00A0 \\2014';\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #ffffff;\n background-color: #333333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n word-break: break-all;\n word-wrap: break-word;\n color: #333333;\n background-color: #f5f5f5;\n border: 1px solid #cccccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n.row {\n margin-left: -15px;\n margin-right: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #dddddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #dddddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #dddddd;\n}\n.table .table {\n background-color: #ffffff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #dddddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #dddddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-of-type(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #dddddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n min-width: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: bold;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #ffffff;\n background-image: none;\n border: 1px solid #cccccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999999;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n background-color: #eeeeee;\n opacity: 1;\n}\n.form-control[disabled],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n}\ntextarea.form-control {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"].form-control,\n input[type=\"time\"].form-control,\n input[type=\"datetime-local\"].form-control,\n input[type=\"month\"].form-control {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm,\n .input-group-sm input[type=\"date\"],\n .input-group-sm input[type=\"time\"],\n .input-group-sm input[type=\"datetime-local\"],\n .input-group-sm input[type=\"month\"] {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg,\n .input-group-lg input[type=\"date\"],\n .input-group-lg input[type=\"time\"],\n .input-group-lg input[type=\"datetime-local\"],\n .input-group-lg input[type=\"month\"] {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.form-control-static {\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n min-height: 34px;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-left: 0;\n padding-right: 0;\n}\n.input-sm {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\nselect[multiple].input-sm {\n height: auto;\n}\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.form-group-sm select.form-control {\n height: 30px;\n line-height: 30px;\n}\n.form-group-sm textarea.form-control,\n.form-group-sm select[multiple].form-control {\n height: auto;\n}\n.form-group-sm .form-control-static {\n height: 30px;\n min-height: 32px;\n padding: 6px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.input-lg {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-lg {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\nselect[multiple].input-lg {\n height: auto;\n}\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.form-group-lg select.form-control {\n height: 46px;\n line-height: 46px;\n}\n.form-group-lg textarea.form-control,\n.form-group-lg select[multiple].form-control {\n height: auto;\n}\n.form-group-lg .form-control-static {\n height: 46px;\n min-height: 38px;\n padding: 11px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback,\n.input-group-lg + .form-control-feedback,\n.form-group-lg .form-control + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback,\n.input-group-sm + .form-control-feedback,\n.form-group-sm .form-control + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n border-color: #3c763d;\n background-color: #dff0d8;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n border-color: #8a6d3b;\n background-color: #fcf8e3;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n border-color: #a94442;\n background-color: #f2dede;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n margin-top: 0;\n margin-bottom: 0;\n padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-left: -15px;\n margin-right: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n text-align: right;\n margin-bottom: 0;\n padding-top: 7px;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 14.333333px;\n font-size: 18px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n font-size: 12px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n white-space: nowrap;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n outline: 0;\n background-image: none;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n opacity: 0.65;\n filter: alpha(opacity=65);\n -webkit-box-shadow: none;\n box-shadow: none;\n}\na.btn.disabled,\nfieldset[disabled] a.btn {\n pointer-events: none;\n}\n.btn-default {\n color: #333333;\n background-color: #ffffff;\n border-color: #cccccc;\n}\n.btn-default:focus,\n.btn-default.focus {\n color: #333333;\n background-color: #e6e6e6;\n border-color: #8c8c8c;\n}\n.btn-default:hover {\n color: #333333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active:hover,\n.btn-default.active:hover,\n.open > .dropdown-toggle.btn-default:hover,\n.btn-default:active:focus,\n.btn-default.active:focus,\n.open > .dropdown-toggle.btn-default:focus,\n.btn-default:active.focus,\n.btn-default.active.focus,\n.open > .dropdown-toggle.btn-default.focus {\n color: #333333;\n background-color: #d4d4d4;\n border-color: #8c8c8c;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n background-image: none;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #ffffff;\n border-color: #cccccc;\n}\n.btn-default .badge {\n color: #ffffff;\n background-color: #333333;\n}\n.btn-primary {\n color: #ffffff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:focus,\n.btn-primary.focus {\n color: #ffffff;\n background-color: #286090;\n border-color: #122b40;\n}\n.btn-primary:hover {\n color: #ffffff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #ffffff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active:hover,\n.btn-primary.active:hover,\n.open > .dropdown-toggle.btn-primary:hover,\n.btn-primary:active:focus,\n.btn-primary.active:focus,\n.open > .dropdown-toggle.btn-primary:focus,\n.btn-primary:active.focus,\n.btn-primary.active.focus,\n.open > .dropdown-toggle.btn-primary.focus {\n color: #ffffff;\n background-color: #204d74;\n border-color: #122b40;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n background-image: none;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.btn-success {\n color: #ffffff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:focus,\n.btn-success.focus {\n color: #ffffff;\n background-color: #449d44;\n border-color: #255625;\n}\n.btn-success:hover {\n color: #ffffff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #ffffff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active:hover,\n.btn-success.active:hover,\n.open > .dropdown-toggle.btn-success:hover,\n.btn-success:active:focus,\n.btn-success.active:focus,\n.open > .dropdown-toggle.btn-success:focus,\n.btn-success:active.focus,\n.btn-success.active.focus,\n.open > .dropdown-toggle.btn-success.focus {\n color: #ffffff;\n background-color: #398439;\n border-color: #255625;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n background-image: none;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #ffffff;\n}\n.btn-info {\n color: #ffffff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:focus,\n.btn-info.focus {\n color: #ffffff;\n background-color: #31b0d5;\n border-color: #1b6d85;\n}\n.btn-info:hover {\n color: #ffffff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #ffffff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active:hover,\n.btn-info.active:hover,\n.open > .dropdown-toggle.btn-info:hover,\n.btn-info:active:focus,\n.btn-info.active:focus,\n.open > .dropdown-toggle.btn-info:focus,\n.btn-info:active.focus,\n.btn-info.active.focus,\n.open > .dropdown-toggle.btn-info.focus {\n color: #ffffff;\n background-color: #269abc;\n border-color: #1b6d85;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n background-image: none;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #ffffff;\n}\n.btn-warning {\n color: #ffffff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:focus,\n.btn-warning.focus {\n color: #ffffff;\n background-color: #ec971f;\n border-color: #985f0d;\n}\n.btn-warning:hover {\n color: #ffffff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #ffffff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active:hover,\n.btn-warning.active:hover,\n.open > .dropdown-toggle.btn-warning:hover,\n.btn-warning:active:focus,\n.btn-warning.active:focus,\n.open > .dropdown-toggle.btn-warning:focus,\n.btn-warning:active.focus,\n.btn-warning.active.focus,\n.open > .dropdown-toggle.btn-warning.focus {\n color: #ffffff;\n background-color: #d58512;\n border-color: #985f0d;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n background-image: none;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #ffffff;\n}\n.btn-danger {\n color: #ffffff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:focus,\n.btn-danger.focus {\n color: #ffffff;\n background-color: #c9302c;\n border-color: #761c19;\n}\n.btn-danger:hover {\n color: #ffffff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #ffffff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active:hover,\n.btn-danger.active:hover,\n.open > .dropdown-toggle.btn-danger:hover,\n.btn-danger:active:focus,\n.btn-danger.active:focus,\n.open > .dropdown-toggle.btn-danger:focus,\n.btn-danger:active.focus,\n.btn-danger.active.focus,\n.open > .dropdown-toggle.btn-danger.focus {\n color: #ffffff;\n background-color: #ac2925;\n border-color: #761c19;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n background-image: none;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #ffffff;\n}\n.btn-link {\n color: #337ab7;\n font-weight: normal;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n}\n.collapse.in {\n display: block;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px dashed;\n border-top: 4px solid \\9;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropup,\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n list-style: none;\n font-size: 14px;\n text-align: left;\n background-color: #ffffff;\n border: 1px solid #cccccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #ffffff;\n text-decoration: none;\n outline: 0;\n background-color: #337ab7;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n cursor: not-allowed;\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n left: auto;\n right: 0;\n}\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n border-top: 0;\n border-bottom: 4px dashed;\n border-bottom: 4px solid \\9;\n content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 2px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n left: auto;\n right: 0;\n }\n .navbar-right .dropdown-menu-left {\n left: 0;\n right: auto;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn,\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-bottom-left-radius: 4px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: normal;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #cccccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n z-index: 2;\n margin-left: -1px;\n}\n.nav {\n margin-bottom: 0;\n padding-left: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n background-color: transparent;\n cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #dddddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #dddddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-bottom-color: transparent;\n cursor: default;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #dddddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #ffffff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #ffffff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #dddddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #ffffff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n}\n.tab-content > .active {\n display: block;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n overflow-x: visible;\n padding-right: 15px;\n padding-left: 15px;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-left: 0;\n padding-right: 0;\n }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.navbar-brand {\n float: left;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n height: 50px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: 15px;\n padding: 9px 10px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n margin-left: -15px;\n margin-right: -15px;\n padding: 10px 15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n margin-bottom: 0;\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-left: 15px;\n margin-right: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #cccccc;\n background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n border-color: #dddddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #dddddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n background-color: #e7e7e7;\n color: #555555;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #cccccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-link {\n color: #777777;\n}\n.navbar-default .navbar-link:hover {\n color: #333333;\n}\n.navbar-default .btn-link {\n color: #777777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #cccccc;\n}\n.navbar-inverse {\n background-color: #222222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #ffffff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #ffffff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #ffffff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #ffffff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n background-color: #080808;\n color: #ffffff;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #ffffff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #ffffff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #ffffff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #ffffff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n content: \"/\\00a0\";\n padding: 0 5px;\n color: #cccccc;\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n line-height: 1.42857143;\n text-decoration: none;\n color: #337ab7;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-bottom-left-radius: 4px;\n border-top-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-bottom-right-radius: 4px;\n border-top-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n z-index: 3;\n color: #23527c;\n background-color: #eeeeee;\n border-color: #dddddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 2;\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n background-color: #ffffff;\n border-color: #dddddd;\n cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.3333333;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-bottom-left-radius: 6px;\n border-top-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-bottom-right-radius: 6px;\n border-top-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-bottom-left-radius: 3px;\n border-top-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-bottom-right-radius: 3px;\n border-top-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n list-style: none;\n text-align: center;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n background-color: #ffffff;\n cursor: not-allowed;\n}\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: #ffffff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n color: #ffffff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n color: #ffffff;\n line-height: 1;\n vertical-align: middle;\n white-space: nowrap;\n text-align: center;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge,\n.btn-group-xs > .btn .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #ffffff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding-top: 30px;\n padding-bottom: 30px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding-top: 48px;\n padding-bottom: 48px;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-left: 60px;\n padding-right: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-left: auto;\n margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n background-color: #dff0d8;\n border-color: #d6e9c6;\n color: #3c763d;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n background-color: #d9edf7;\n border-color: #bce8f1;\n color: #31708f;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n background-color: #fcf8e3;\n border-color: #faebcc;\n color: #8a6d3b;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n background-color: #f2dede;\n border-color: #ebccd1;\n color: #a94442;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n overflow: hidden;\n height: 20px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #ffffff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media,\n.media-body {\n zoom: 1;\n overflow: hidden;\n}\n.media-body {\n width: 10000px;\n}\n.media-object {\n display: block;\n}\n.media-object.img-thumbnail {\n max-width: none;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n margin-bottom: 20px;\n padding-left: 0;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n}\n.list-group-item:first-child {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\na.list-group-item,\nbutton.list-group-item {\n color: #555555;\n}\na.list-group-item .list-group-item-heading,\nbutton.list-group-item .list-group-item-heading {\n color: #333333;\n}\na.list-group-item:hover,\nbutton.list-group-item:hover,\na.list-group-item:focus,\nbutton.list-group-item:focus {\n text-decoration: none;\n color: #555555;\n background-color: #f5f5f5;\n}\nbutton.list-group-item {\n width: 100%;\n text-align: left;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n background-color: #eeeeee;\n color: #777777;\n cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success,\nbutton.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading,\nbutton.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\nbutton.list-group-item-success:hover,\na.list-group-item-success:focus,\nbutton.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\nbutton.list-group-item-success.active,\na.list-group-item-success.active:hover,\nbutton.list-group-item-success.active:hover,\na.list-group-item-success.active:focus,\nbutton.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info,\nbutton.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading,\nbutton.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\nbutton.list-group-item-info:hover,\na.list-group-item-info:focus,\nbutton.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\nbutton.list-group-item-info.active,\na.list-group-item-info.active:hover,\nbutton.list-group-item-info.active:hover,\na.list-group-item-info.active:focus,\nbutton.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning,\nbutton.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading,\nbutton.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\nbutton.list-group-item-warning:hover,\na.list-group-item-warning:focus,\nbutton.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\nbutton.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\nbutton.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus,\nbutton.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger,\nbutton.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading,\nbutton.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\nbutton.list-group-item-danger:hover,\na.list-group-item-danger:focus,\nbutton.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\nbutton.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\nbutton.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus,\nbutton.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #ffffff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a,\n.panel-title > small,\n.panel-title > .small,\n.panel-title > small > a,\n.panel-title > .small > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #dddddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .panel-heading + .panel-collapse > .list-group .list-group-item:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-left: 15px;\n padding-right: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-left-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #dddddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n border: 0;\n margin-bottom: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #dddddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #dddddd;\n}\n.panel-default {\n border-color: #dddddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #dddddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #dddddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #dddddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n}\n.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000000;\n text-shadow: 0 1px 0 #ffffff;\n opacity: 0.2;\n filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n color: #000000;\n text-decoration: none;\n cursor: pointer;\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1050;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #ffffff;\n border: 1px solid #999999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n background-clip: padding-box;\n outline: 0;\n}\n.modal-backdrop {\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n background-color: #000000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n min-height: 16.42857143px;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n font-size: 12px;\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.tooltip.in {\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.tooltip.top {\n margin-top: -3px;\n padding: 5px 0;\n}\n.tooltip.right {\n margin-left: 3px;\n padding: 0 5px;\n}\n.tooltip.bottom {\n margin-top: 3px;\n padding: 5px 0;\n}\n.tooltip.left {\n margin-left: -3px;\n padding: 0 5px;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #ffffff;\n text-align: center;\n background-color: #000000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.top-left .tooltip-arrow {\n bottom: 0;\n right: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-style: normal;\n font-weight: normal;\n letter-spacing: normal;\n line-break: auto;\n line-height: 1.42857143;\n text-align: left;\n text-align: start;\n text-decoration: none;\n text-shadow: none;\n text-transform: none;\n white-space: normal;\n word-break: normal;\n word-spacing: normal;\n word-wrap: normal;\n font-size: 14px;\n background-color: #ffffff;\n background-clip: padding-box;\n border: 1px solid #cccccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover-title {\n margin: 0;\n padding: 8px 14px;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow:after {\n border-width: 10px;\n content: \"\";\n}\n.popover.top > .arrow {\n left: 50%;\n margin-left: -11px;\n border-bottom-width: 0;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n bottom: -11px;\n}\n.popover.top > .arrow:after {\n content: \" \";\n bottom: 1px;\n margin-left: -10px;\n border-bottom-width: 0;\n border-top-color: #ffffff;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-left-width: 0;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n content: \" \";\n left: 1px;\n bottom: -10px;\n border-left-width: 0;\n border-right-color: #ffffff;\n}\n.popover.bottom > .arrow {\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n top: -11px;\n}\n.popover.bottom > .arrow:after {\n content: \" \";\n top: 1px;\n margin-left: -10px;\n border-top-width: 0;\n border-bottom-color: #ffffff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: #ffffff;\n bottom: -10px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n}\n.carousel-inner > .item {\n display: none;\n position: relative;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n -webkit-transition: -webkit-transform 0.6s ease-in-out;\n -moz-transition: -moz-transform 0.6s ease-in-out;\n -o-transition: -o-transform 0.6s ease-in-out;\n transition: transform 0.6s ease-in-out;\n -webkit-backface-visibility: hidden;\n -moz-backface-visibility: hidden;\n backface-visibility: hidden;\n -webkit-perspective: 1000px;\n -moz-perspective: 1000px;\n perspective: 1000px;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n -webkit-transform: translate3d(100%, 0, 0);\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n -webkit-transform: translate3d(-100%, 0, 0);\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n -webkit-transform: translate3d(0, 0, 0);\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 15%;\n opacity: 0.5;\n filter: alpha(opacity=50);\n font-size: 20px;\n color: #ffffff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n left: auto;\n right: 0;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n outline: 0;\n color: #ffffff;\n text-decoration: none;\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n margin-top: -10px;\n z-index: 5;\n display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n line-height: 1;\n font-family: serif;\n}\n.carousel-control .icon-prev:before {\n content: '\\2039';\n}\n.carousel-control .icon-next:before {\n content: '\\203a';\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid #ffffff;\n border-radius: 10px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: #ffffff;\n}\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #ffffff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -15px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -15px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -15px;\n }\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-footer:before,\n.modal-footer:after {\n content: \" \";\n display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table !important;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table !important;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table !important;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table !important;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table !important;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS and IE text size adjust after device orientation change,\n// without disabling user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability of focused elements when they are also in an\n// active/hover state.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n//\n\nabbr[title] {\n border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome.\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n box-sizing: content-box; //2\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important; // Black prints faster: h5bp.com/s\n box-shadow: none !important;\n text-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n // Bootstrap specific changes end\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('@{icon-font-path}@{icon-font-name}.eot');\n src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'),\n url('@{icon-font-path}@{icon-font-name}.woff2') format('woff2'),\n url('@{icon-font-path}@{icon-font-name}.woff') format('woff'),\n url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'),\n url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg');\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\2a\"; } }\n.glyphicon-plus { &:before { content: \"\\2b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n.glyphicon-cd { &:before { content: \"\\e201\"; } }\n.glyphicon-save-file { &:before { content: \"\\e202\"; } }\n.glyphicon-open-file { &:before { content: \"\\e203\"; } }\n.glyphicon-level-up { &:before { content: \"\\e204\"; } }\n.glyphicon-copy { &:before { content: \"\\e205\"; } }\n.glyphicon-paste { &:before { content: \"\\e206\"; } }\n// The following 2 Glyphicons are omitted for the time being because\n// they currently use Unicode codepoints that are outside the\n// Basic Multilingual Plane (BMP). Older buggy versions of WebKit can't handle\n// non-BMP codepoints in CSS string escapes, and thus can't display these two icons.\n// Notably, the bug affects some older versions of the Android Browser.\n// More info: https://github.com/twbs/bootstrap/issues/10106\n// .glyphicon-door { &:before { content: \"\\1f6aa\"; } }\n// .glyphicon-key { &:before { content: \"\\1f511\"; } }\n.glyphicon-alert { &:before { content: \"\\e209\"; } }\n.glyphicon-equalizer { &:before { content: \"\\e210\"; } }\n.glyphicon-king { &:before { content: \"\\e211\"; } }\n.glyphicon-queen { &:before { content: \"\\e212\"; } }\n.glyphicon-pawn { &:before { content: \"\\e213\"; } }\n.glyphicon-bishop { &:before { content: \"\\e214\"; } }\n.glyphicon-knight { &:before { content: \"\\e215\"; } }\n.glyphicon-baby-formula { &:before { content: \"\\e216\"; } }\n.glyphicon-tent { &:before { content: \"\\26fa\"; } }\n.glyphicon-blackboard { &:before { content: \"\\e218\"; } }\n.glyphicon-bed { &:before { content: \"\\e219\"; } }\n.glyphicon-apple { &:before { content: \"\\f8ff\"; } }\n.glyphicon-erase { &:before { content: \"\\e221\"; } }\n.glyphicon-hourglass { &:before { content: \"\\231b\"; } }\n.glyphicon-lamp { &:before { content: \"\\e223\"; } }\n.glyphicon-duplicate { &:before { content: \"\\e224\"; } }\n.glyphicon-piggy-bank { &:before { content: \"\\e225\"; } }\n.glyphicon-scissors { &:before { content: \"\\e226\"; } }\n.glyphicon-bitcoin { &:before { content: \"\\e227\"; } }\n.glyphicon-btc { &:before { content: \"\\e227\"; } }\n.glyphicon-xbt { &:before { content: \"\\e227\"; } }\n.glyphicon-yen { &:before { content: \"\\00a5\"; } }\n.glyphicon-jpy { &:before { content: \"\\00a5\"; } }\n.glyphicon-ruble { &:before { content: \"\\20bd\"; } }\n.glyphicon-rub { &:before { content: \"\\20bd\"; } }\n.glyphicon-scale { &:before { content: \"\\e230\"; } }\n.glyphicon-ice-lolly { &:before { content: \"\\e231\"; } }\n.glyphicon-ice-lolly-tasted { &:before { content: \"\\e232\"; } }\n.glyphicon-education { &:before { content: \"\\e233\"; } }\n.glyphicon-option-horizontal { &:before { content: \"\\e234\"; } }\n.glyphicon-option-vertical { &:before { content: \"\\e235\"; } }\n.glyphicon-menu-hamburger { &:before { content: \"\\e236\"; } }\n.glyphicon-modal-window { &:before { content: \"\\e237\"; } }\n.glyphicon-oil { &:before { content: \"\\e238\"; } }\n.glyphicon-grain { &:before { content: \"\\e239\"; } }\n.glyphicon-sunglasses { &:before { content: \"\\e240\"; } }\n.glyphicon-text-size { &:before { content: \"\\e241\"; } }\n.glyphicon-text-color { &:before { content: \"\\e242\"; } }\n.glyphicon-text-background { &:before { content: \"\\e243\"; } }\n.glyphicon-object-align-top { &:before { content: \"\\e244\"; } }\n.glyphicon-object-align-bottom { &:before { content: \"\\e245\"; } }\n.glyphicon-object-align-horizontal{ &:before { content: \"\\e246\"; } }\n.glyphicon-object-align-left { &:before { content: \"\\e247\"; } }\n.glyphicon-object-align-vertical { &:before { content: \"\\e248\"; } }\n.glyphicon-object-align-right { &:before { content: \"\\e249\"; } }\n.glyphicon-triangle-right { &:before { content: \"\\e250\"; } }\n.glyphicon-triangle-left { &:before { content: \"\\e251\"; } }\n.glyphicon-triangle-bottom { &:before { content: \"\\e252\"; } }\n.glyphicon-triangle-top { &:before { content: \"\\e253\"; } }\n.glyphicon-console { &:before { content: \"\\e254\"; } }\n.glyphicon-superscript { &:before { content: \"\\e255\"; } }\n.glyphicon-subscript { &:before { content: \"\\e256\"; } }\n.glyphicon-menu-left { &:before { content: \"\\e257\"; } }\n.glyphicon-menu-right { &:before { content: \"\\e258\"; } }\n.glyphicon-menu-down { &:before { content: \"\\e259\"; } }\n.glyphicon-menu-up { &:before { content: \"\\e260\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content/\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n\n\n// iOS \"clickable elements\" fix for role=\"button\"\n//\n// Fixes \"clickability\" issue (and more generally, the firing of events such as focus as well)\n// for traditionally non-focusable elements with role=\"button\"\n// see https://developer.mozilla.org/en-US/docs/Web/Events/click#Safari_Mobile\n\n[role=\"button\"] {\n cursor: pointer;\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // Override Firefox's unusual default opacity; see https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: normal;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n background-color: @state-warning-bg;\n padding: .2em;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @grid-float-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n font-size: 90%;\n .text-uppercase();\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: '\\2014 \\00A0'; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n text-align: right;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: ''; }\n &:after {\n content: '\\00A0 \\2014'; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover,\n a&:focus {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover,\n a&:focus {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n word-break: break-all;\n word-wrap: break-word;\n color: @pre-color;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: ceil((@gutter / -2));\n margin-right: floor((@gutter / -2));\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: ceil((@grid-gutter-width / 2));\n padding-right: floor((@grid-gutter-width / 2));\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-of-type(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-column;\n}\ntable {\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9-11 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-cell;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * 0.75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n}\n\ninput[type=\"file\"] {\n display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n .tab-focus();\n}\n\n// Adjust output element\noutput {\n display: block;\n padding-top: (@padding-base-vertical + 1);\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n display: block;\n width: 100%;\n height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n background-color: @input-bg;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid @input-border;\n border-radius: @input-border-radius; // Note: This has no effect on s in CSS.\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n // Customize the `:focus` state to imitate native WebKit styles.\n .form-control-focus();\n\n // Placeholder\n .placeholder();\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &[disabled],\n &[readonly],\n fieldset[disabled] & {\n background-color: @input-bg-disabled;\n opacity: 1; // iOS fix for unreadable disabled content; see https://github.com/twbs/bootstrap/issues/11655\n }\n\n &[disabled],\n fieldset[disabled] & {\n cursor: @cursor-disabled;\n }\n\n // Reset height for `textarea`s\n textarea& {\n height: auto;\n }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n\n\n// Special styles for iOS temporal inputs\n//\n// In Mobile Safari, setting `display: block` on temporal inputs causes the\n// text within the input to become vertically misaligned. As a workaround, we\n// set a pixel line-height that matches the given height of the input, but only\n// for Safari. See https://bugs.webkit.org/show_bug.cgi?id=139848\n//\n// Note that as of 8.3, iOS doesn't support `datetime` or `week`.\n\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"],\n input[type=\"time\"],\n input[type=\"datetime-local\"],\n input[type=\"month\"] {\n &.form-control {\n line-height: @input-height-base;\n }\n\n &.input-sm,\n .input-group-sm & {\n line-height: @input-height-small;\n }\n\n &.input-lg,\n .input-group-lg & {\n line-height: @input-height-large;\n }\n }\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n margin-bottom: @form-group-margin-bottom;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n\n label {\n min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n position: relative;\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n// Some special care is needed because