Przeglądaj źródła

Initial user authentication and user account database synchronisation now working.

Andrew Klopper 8 lat temu
rodzic
commit
88b191fb13

+ 12 - 0
BulkPrintingAPI.sln

@@ -5,6 +5,10 @@ VisualStudioVersion = 15.0.26430.13
5 5
 MinimumVisualStudioVersion = 10.0.40219.1
6 6
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BulkPrintingAPI", "BulkPrintingAPI\BulkPrintingAPI.csproj", "{92EEB85E-10FF-4902-9F73-BA4D58344824}"
7 7
 EndProject
8
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MAXClient", "MAXClient\MAXClient.csproj", "{DDE80401-F7D7-4006-AC06-2FDB57C77798}"
9
+EndProject
10
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MAXModels", "MAXData\MAXModels.csproj", "{2F570746-72B2-495E-AFEE-085E1A8E4CA8}"
11
+EndProject
8 12
 Global
9 13
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 14
 		Debug|Any CPU = Debug|Any CPU
@@ -15,6 +19,14 @@ Global
15 19
 		{92EEB85E-10FF-4902-9F73-BA4D58344824}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 20
 		{92EEB85E-10FF-4902-9F73-BA4D58344824}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 21
 		{92EEB85E-10FF-4902-9F73-BA4D58344824}.Release|Any CPU.Build.0 = Release|Any CPU
22
+		{DDE80401-F7D7-4006-AC06-2FDB57C77798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23
+		{DDE80401-F7D7-4006-AC06-2FDB57C77798}.Debug|Any CPU.Build.0 = Debug|Any CPU
24
+		{DDE80401-F7D7-4006-AC06-2FDB57C77798}.Release|Any CPU.ActiveCfg = Release|Any CPU
25
+		{DDE80401-F7D7-4006-AC06-2FDB57C77798}.Release|Any CPU.Build.0 = Release|Any CPU
26
+		{2F570746-72B2-495E-AFEE-085E1A8E4CA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27
+		{2F570746-72B2-495E-AFEE-085E1A8E4CA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
28
+		{2F570746-72B2-495E-AFEE-085E1A8E4CA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
29
+		{2F570746-72B2-495E-AFEE-085E1A8E4CA8}.Release|Any CPU.Build.0 = Release|Any CPU
18 30
 	EndGlobalSection
19 31
 	GlobalSection(SolutionProperties) = preSolution
20 32
 		HideSolutionNode = FALSE

+ 0 - 134
BulkPrintingAPI/Authentication/TokenProviderMiddleware.cs

@@ -1,134 +0,0 @@
1
-using Microsoft.AspNetCore.Http;
2
-using Microsoft.Extensions.Options;
3
-using Newtonsoft.Json;
4
-using System;
5
-using System.IdentityModel.Tokens.Jwt;
6
-using System.Security.Claims;
7
-using System.Threading.Tasks;
8
-
9
-namespace BulkPrintingAPI.Authentication
10
-{
11
-    public class TokenProviderMiddleware
12
-    {
13
-        private readonly RequestDelegate _next;
14
-        private readonly TokenProviderOptions _options;
15
-        private readonly JsonSerializerSettings _serializerSettings;
16
-
17
-        public TokenProviderMiddleware(
18
-            RequestDelegate next,
19
-            IOptions<TokenProviderOptions> options)
20
-        {
21
-            _next = next;
22
-
23
-            _options = options.Value;
24
-            ThrowIfInvalidOptions(_options);
25
-
26
-            _serializerSettings = new JsonSerializerSettings
27
-            {
28
-                Formatting = Formatting.Indented
29
-            };
30
-        }
31
-
32
-        public Task Invoke(HttpContext context)
33
-        {
34
-            if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
35
-            {
36
-                return _next(context);
37
-            }
38
-
39
-            if (!context.Request.Method.Equals("POST")
40
-               || !context.Request.HasFormContentType)
41
-            {
42
-                context.Response.StatusCode = 400;
43
-                return context.Response.WriteAsync("Bad request.");
44
-            }
45
-
46
-            return GenerateToken(context);
47
-        }
48
-
49
-        private async Task GenerateToken(HttpContext context)
50
-        {
51
-            var username = context.Request.Form["username"];
52
-            var password = context.Request.Form["password"];
53
-
54
-            var identity = await _options.IdentityResolver(username, password);
55
-            if (identity == null)
56
-            {
57
-                context.Response.StatusCode = 400;
58
-                await context.Response.WriteAsync("Invalid username or password.");
59
-                return;
60
-            }
61
-
62
-            var now = DateTime.UtcNow;
63
-
64
-            var claims = new Claim[]
65
-            {
66
-                new Claim(JwtRegisteredClaimNames.Sub, username),
67
-                new Claim(JwtRegisteredClaimNames.Jti, await _options.NonceGenerator())
68
-            };
69
-
70
-            var encodedJwt = new JwtSecurityTokenHandler().CreateEncodedJwt(
71
-                issuer: _options.Issuer,
72
-                audience: _options.Audience,
73
-                subject: new ClaimsIdentity(claims),
74
-                notBefore: now,
75
-                expires: now.Add(_options.Expiration),
76
-                issuedAt: now,
77
-                signingCredentials: _options.SigningCredentials,
78
-                encryptingCredentials: _options.EncryptingCredentials
79
-            );
80
-
81
-            var response = new
82
-            {
83
-                access_token = encodedJwt,
84
-                expires_in = (int)_options.Expiration.TotalSeconds
85
-            };
86
-
87
-            context.Response.ContentType = "application/json";
88
-            await context.Response.WriteAsync(JsonConvert.SerializeObject(response, _serializerSettings));
89
-        }
90
-
91
-        private static void ThrowIfInvalidOptions(TokenProviderOptions options)
92
-        {
93
-            if (string.IsNullOrEmpty(options.Path))
94
-            {
95
-                throw new ArgumentNullException(nameof(TokenProviderOptions.Path));
96
-            }
97
-
98
-            if (string.IsNullOrEmpty(options.Issuer))
99
-            {
100
-                throw new ArgumentNullException(nameof(TokenProviderOptions.Issuer));
101
-            }
102
-
103
-            if (string.IsNullOrEmpty(options.Audience))
104
-            {
105
-                throw new ArgumentNullException(nameof(TokenProviderOptions.Audience));
106
-            }
107
-
108
-            if (options.Expiration == TimeSpan.Zero)
109
-            {
110
-                throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(TokenProviderOptions.Expiration));
111
-            }
112
-
113
-            if (options.IdentityResolver == null)
114
-            {
115
-                throw new ArgumentNullException(nameof(TokenProviderOptions.IdentityResolver));
116
-            }
117
-
118
-            if (options.SigningCredentials == null)
119
-            {
120
-                throw new ArgumentNullException(nameof(TokenProviderOptions.SigningCredentials));
121
-            }
122
-
123
-            if (options.EncryptingCredentials == null)
124
-            {
125
-                throw new ArgumentNullException(nameof(TokenProviderOptions.EncryptingCredentials));
126
-            }
127
-
128
-            if (options.NonceGenerator == null)
129
-            {
130
-                throw new ArgumentNullException(nameof(TokenProviderOptions.NonceGenerator));
131
-            }
132
-        }
133
-    }
134
-}

+ 0 - 54
BulkPrintingAPI/Authentication/TokenProviderOptions.cs

@@ -1,54 +0,0 @@
1
-using Microsoft.IdentityModel.Tokens;
2
-using System;
3
-using System.Security.Claims;
4
-using System.Threading.Tasks;
5
-
6
-namespace BulkPrintingAPI.Authentication
7
-{
8
-    public class TokenProviderOptions
9
-    {
10
-        /// <summary>
11
-        /// The relative request path to listen on.
12
-        /// </summary>
13
-        /// <remarks>The default path is <c>/token</c>.</remarks>
14
-        public string Path { get; set; } = "/token";
15
-
16
-        /// <summary>
17
-        ///  The Issuer (iss) claim for generated tokens.
18
-        /// </summary>
19
-        public string Issuer { get; set; }
20
-
21
-        /// <summary>
22
-        /// The Audience (aud) claim for the generated tokens.
23
-        /// </summary>
24
-        public string Audience { get; set; }
25
-
26
-        /// <summary>
27
-        /// The expiration time for the generated tokens.
28
-        /// </summary>
29
-        /// <remarks>The default is five minutes (300 seconds).</remarks>
30
-        public TimeSpan Expiration { get; set; } = TimeSpan.FromDays(1);
31
-
32
-        /// <summary>
33
-        /// The signing key to use when generating tokens.
34
-        /// </summary>
35
-        public SigningCredentials SigningCredentials { get; set; }
36
-
37
-        /// <summary>
38
-        /// The encryption key to use when generating tokens.
39
-        /// </summary>
40
-        public EncryptingCredentials EncryptingCredentials { get; set; }
41
-
42
-        /// <summary>
43
-        /// Resolves a user identity given a username and password.
44
-        /// </summary>
45
-        public Func<string, string, Task<ClaimsIdentity>> IdentityResolver { get; set; }
46
-
47
-        /// <summary>
48
-        /// Generates a random value (nonce) for each generated token.
49
-        /// </summary>
50
-        /// <remarks>The default nonce is a random GUID.</remarks>
51
-        public Func<Task<string>> NonceGenerator { get; set; }
52
-            = () => Task.FromResult(Guid.NewGuid().ToString());
53
-    }
54
-}

+ 15 - 1
BulkPrintingAPI/BulkPrintingAPI.csproj

@@ -2,10 +2,11 @@
2 2
 
3 3
   <PropertyGroup>
4 4
     <TargetFramework>netcoreapp1.1</TargetFramework>
5
+    <PackageTargetFallback>portable-net45+win8</PackageTargetFallback>
5 6
   </PropertyGroup>
6 7
 
7 8
   <ItemGroup>
8
-    <Folder Include="MAX\" />
9
+    <Folder Include="Models\" />
9 10
     <Folder Include="wwwroot\" />
10 11
   </ItemGroup>
11 12
   <ItemGroup>
@@ -13,7 +14,16 @@
13 14
     <PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
14 15
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.2" />
15 16
     <PackageReference Include="Microsoft.AspNetCore.Mvc" Version="1.1.3" />
17
+    <PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="1.1.2" />
18
+    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" />
19
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.2" />
20
+    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="1.1.2" />
21
+    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.2" />
22
+    <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.1" />
16 23
     <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
24
+    <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" />
25
+    <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.2" />
26
+    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="1.1.1" />
17 27
   </ItemGroup>
18 28
   <ItemGroup>
19 29
     <DotNetCliToolReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Tools" Version="1.0.1" />
@@ -21,5 +31,9 @@
21 31
   <ItemGroup>
22 32
     <None Include="secrets.Development.json" />
23 33
   </ItemGroup>
34
+  <ItemGroup>
35
+    <ProjectReference Include="..\MAXClient\MAXClient.csproj" />
36
+    <ProjectReference Include="..\MAXData\MAXModels.csproj" />
37
+  </ItemGroup>
24 38
 
25 39
 </Project>

+ 51 - 0
BulkPrintingAPI/Configuration/TokenAuthenticationOptions.cs

@@ -0,0 +1,51 @@
1
+using Microsoft.Extensions.Configuration;
2
+using Microsoft.IdentityModel.Tokens;
3
+using System;
4
+using System.Security.Cryptography;
5
+using System.Text;
6
+using System.Threading.Tasks;
7
+
8
+namespace BulkPrintingAPI.Configuration
9
+{
10
+    public class TokenAuthenticationOptions
11
+    {
12
+        public TokenAuthenticationOptions(IConfiguration configuration)
13
+        {
14
+            string secretKey = configuration["SecretKey"];
15
+            if (String.IsNullOrEmpty(secretKey))
16
+                throw new ArgumentException("Missing 'SecretKey' value", nameof(configuration));
17
+
18
+            string salt = configuration["Salt"];
19
+            if (String.IsNullOrEmpty(secretKey))
20
+                throw new ArgumentException("Missing 'Salt' value", nameof(configuration));
21
+
22
+            var rawKey = new Rfc2898DeriveBytes(
23
+                    Encoding.ASCII.GetBytes(secretKey),
24
+                    Encoding.ASCII.GetBytes(salt),
25
+                    configuration.GetValue("Iterations", 4096)).GetBytes(32);
26
+
27
+            Issuer = configuration.GetValue("Issuer", "bulk");
28
+            Audience = configuration.GetValue("Audience", "bulk");
29
+            Lifetime = TimeSpan.FromSeconds(configuration.GetValue("TokenLifetime", 600));
30
+            Key = new SymmetricSecurityKey(rawKey);
31
+            SigningCredentials = new SigningCredentials(Key, SecurityAlgorithms.HmacSha256);
32
+            EncryptingCredentials = new EncryptingCredentials(Key, "dir",
33
+                SecurityAlgorithms.Aes128CbcHmacSha256);
34
+        }
35
+
36
+        public string Issuer { get; set; }
37
+
38
+        public string Audience { get; set; }
39
+
40
+        public TimeSpan Lifetime { get; set; }
41
+
42
+        public SecurityKey Key { get; set; }
43
+
44
+        public SigningCredentials SigningCredentials { get; set; }
45
+
46
+        public EncryptingCredentials EncryptingCredentials { get; set; }
47
+
48
+        public Func<Task<string>> NonceGenerator { get; set; }
49
+            = () => Task.FromResult(Guid.NewGuid().ToString());
50
+    }
51
+}

+ 128 - 0
BulkPrintingAPI/Controllers/LoginController.cs

@@ -0,0 +1,128 @@
1
+using BulkPrintingAPI.Configuration;
2
+using BulkPrintingAPI.Db;
3
+using BulkPrintingAPI.Services;
4
+using MAX.Models;
5
+using Microsoft.AspNetCore.Authorization;
6
+using Microsoft.AspNetCore.Mvc;
7
+using Microsoft.AspNetCore.Mvc.ModelBinding;
8
+using Microsoft.Extensions.Logging;
9
+using System;
10
+using System.ComponentModel.DataAnnotations;
11
+using System.IdentityModel.Tokens.Jwt;
12
+using System.Security.Claims;
13
+using System.Threading.Tasks;
14
+
15
+namespace BulkPrintingAPI.Controllers
16
+{
17
+    //[Produces("application/json")]
18
+    [Route("api/[controller]")]
19
+    public class LoginController : Controller
20
+    {
21
+        public class LoginRequest
22
+        {
23
+            [Required]
24
+            public int? VendorId { get; set; }
25
+
26
+            [Required]
27
+            public string SerialNumber { get; set; }
28
+
29
+            [Required]
30
+            public int? UserId { get; set; }
31
+
32
+            [Required]
33
+            public string Username { get; set; }
34
+
35
+            [Required]
36
+            public string Password { get; set; }
37
+        }
38
+
39
+        private ILogger logger;
40
+        private TokenAuthenticationOptions tokenAuthenticationOptions;
41
+        private MAXClientFactory clientFactory;
42
+        private MAXDbContext dbContext;
43
+
44
+        public LoginController(ILoggerFactory loggerFactory,
45
+            TokenAuthenticationOptions tokenAuthenticationOptions,
46
+            MAXClientFactory clientFactory,
47
+            MAXDbContext dbContext)
48
+        {
49
+            logger = loggerFactory.CreateLogger(GetType().FullName);
50
+            this.tokenAuthenticationOptions = tokenAuthenticationOptions;
51
+            this.clientFactory = clientFactory;
52
+            this.dbContext = dbContext;
53
+        }
54
+
55
+        [HttpPost]
56
+        [AllowAnonymous]
57
+        public async Task<IActionResult> Post([FromBody] LoginRequest loginRequest)
58
+        {
59
+            if (! ModelState.IsValid)
60
+                return BadRequest();
61
+
62
+            using (var client = clientFactory.GetClient(logger, loginRequest.VendorId.Value,
63
+                loginRequest.SerialNumber, loginRequest.UserId.Value, loginRequest.Username,
64
+                loginRequest.Password))
65
+            {
66
+                User user;
67
+                try
68
+                {
69
+                    user = await client.ConnectAsync();
70
+                }
71
+                catch (Exception e)
72
+                {
73
+                    logger.LogError(
74
+                        "ConnectAsync failed for vendorId={0} serialNumber={1} userId={2} username={3}: {4}",
75
+                        loginRequest.VendorId, loginRequest.SerialNumber,
76
+                        loginRequest.UserId, loginRequest.Username, e.Message);
77
+                    return Unauthorized();
78
+                }
79
+
80
+                if (user == null)
81
+                {
82
+                    logger.LogInformation(
83
+                        "Login failed for vendorId={0} serialNumber={1} userId={2} username={3}",
84
+                        loginRequest.VendorId, loginRequest.SerialNumber,
85
+                        loginRequest.UserId, loginRequest.Username);
86
+                    return Unauthorized();
87
+                }
88
+
89
+                try
90
+                {
91
+                    await DbHelpers.SyncUserAndVendorWithDb(dbContext, user,
92
+                        loginRequest.VendorId.Value,
93
+                        loginRequest.SerialNumber);
94
+                }
95
+                catch (Exception e)
96
+                {
97
+                    logger.LogError(
98
+                        "SyncUserAndVendorWithDb failed for vendorId={0} serialNumber={1} userId={2} username={3}: {4}",
99
+                        loginRequest.VendorId, loginRequest.SerialNumber,
100
+                        loginRequest.UserId, loginRequest.Username, e.Message);
101
+                    return Unauthorized();
102
+                }
103
+
104
+                var now = DateTime.UtcNow;
105
+                var encodedJwt = new JwtSecurityTokenHandler().CreateEncodedJwt(
106
+                    issuer: tokenAuthenticationOptions.Issuer,
107
+                    audience: tokenAuthenticationOptions.Audience,
108
+                    subject: new ClaimsIdentity(new Claim[] {
109
+                       new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
110
+                       new Claim("xpwd", loginRequest.Password),
111
+                       new Claim("xvid", loginRequest.VendorId.ToString()),
112
+                       new Claim(JwtRegisteredClaimNames.Jti, await tokenAuthenticationOptions.NonceGenerator())
113
+                    }),
114
+                    notBefore: now,
115
+                    expires: now.Add(tokenAuthenticationOptions.Lifetime),
116
+                    issuedAt: now,
117
+                    signingCredentials: tokenAuthenticationOptions.SigningCredentials,
118
+                    encryptingCredentials: tokenAuthenticationOptions.EncryptingCredentials
119
+                );
120
+
121
+                return Ok(new {
122
+                    access_token = encodedJwt,
123
+                    expires_in = (int)tokenAuthenticationOptions.Lifetime.TotalSeconds
124
+                });
125
+            }
126
+        }
127
+    }
128
+}

+ 0 - 1
BulkPrintingAPI/Controllers/ValuesController.cs

@@ -9,7 +9,6 @@ namespace BulkPrintingAPI.Controllers
9 9
     {
10 10
         // GET api/values
11 11
         [HttpGet]
12
-        [Authorize]
13 12
         public IEnumerable<string> Get()
14 13
         {
15 14
             return new string[] { "value1", "value2" };

+ 84 - 0
BulkPrintingAPI/Db/DbHelpers.cs

@@ -0,0 +1,84 @@
1
+using MAX.Models;
2
+using System.Threading.Tasks;
3
+
4
+namespace BulkPrintingAPI.Db
5
+{
6
+    public static class DbHelpers
7
+    {
8
+        public static async Task SyncUserAndVendorWithDb(MAXDbContext dbContext,
9
+            User user, int vendorId, string serialNumber)
10
+        {
11
+            var warehouse = user.Account.Warehouse;
12
+            var existingWarehouse = await dbContext.Warehouses.FindAsync(warehouse.Id).ConfigureAwait(false);
13
+            if (existingWarehouse == null)
14
+            {
15
+                dbContext.Warehouses.Add(warehouse);
16
+            }
17
+            else
18
+            {
19
+                existingWarehouse.Name = warehouse.Name;
20
+                warehouse = existingWarehouse;
21
+            }
22
+
23
+            var account = user.Account;
24
+            var existingAccount = await dbContext.Accounts.FindAsync(account.Id).ConfigureAwait(false);
25
+            if (existingAccount ==  null)
26
+            {
27
+                dbContext.Accounts.Add(account);
28
+            }
29
+            else
30
+            {
31
+                existingAccount.Name = account.Name;
32
+                existingAccount.Status = account.Status;
33
+                existingAccount.Reference = account.Reference;
34
+                existingAccount.Balance = account.Balance;
35
+                account = existingAccount;
36
+            }
37
+            account.Warehouse = warehouse;
38
+
39
+            var existingUser = await dbContext.Users.FindAsync(user.Id).ConfigureAwait(false);
40
+            if (existingUser == null)
41
+            {
42
+                dbContext.Users.Add(user);
43
+            }
44
+            else
45
+            {
46
+                existingUser.Username = user.Username;
47
+                existingUser.FirstName = user.FirstName;
48
+                existingUser.Surname = user.Surname;
49
+                existingUser.Enabled = user.Enabled;
50
+                existingUser.Level = user.Level;
51
+                existingUser.System = user.System;
52
+                existingUser.LastLogin = user.LastLogin;
53
+
54
+                existingUser.CanPrintOffline = user.CanPrintOffline;
55
+                existingUser.CanPrintOnline = user.CanPrintOnline;
56
+                existingUser.CanReprintOffline = user.CanReprintOffline;
57
+                existingUser.CanReprintOnline = user.CanReprintOnline;
58
+                existingUser.OfflinePrintValue = user.OfflinePrintValue;
59
+                existingUser.OfflineReprintValue = user.OfflineReprintValue;
60
+                existingUser.OnlinePrintValue = user.OnlinePrintValue;
61
+                existingUser.OnlineReprintValue = user.OnlineReprintValue;
62
+            }
63
+            user.Account = account;
64
+
65
+            var vendor = await dbContext.Vendors.FindAsync(vendorId).ConfigureAwait(false);
66
+            if (vendor == null)
67
+            {
68
+                dbContext.Add(new Vendor()
69
+                {
70
+                    Id = vendorId,
71
+                    SerialNumber = serialNumber,
72
+                    Account = account
73
+                });
74
+            }
75
+            else
76
+            {
77
+                vendor.SerialNumber = serialNumber;
78
+                vendor.Account = account;
79
+            }
80
+
81
+            await dbContext.SaveChangesAsync().ConfigureAwait(false);
82
+        }
83
+    }
84
+}

+ 142 - 0
BulkPrintingAPI/Migrations/20170701135738_InitialCreate.Designer.cs

@@ -0,0 +1,142 @@
1
+using System;
2
+using Microsoft.EntityFrameworkCore;
3
+using Microsoft.EntityFrameworkCore.Infrastructure;
4
+using Microsoft.EntityFrameworkCore.Metadata;
5
+using Microsoft.EntityFrameworkCore.Migrations;
6
+using MAX.Models;
7
+
8
+namespace BulkPrintingAPI.Migrations
9
+{
10
+    [DbContext(typeof(MAXDbContext))]
11
+    [Migration("20170701135738_InitialCreate")]
12
+    partial class InitialCreate
13
+    {
14
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
15
+        {
16
+            modelBuilder
17
+                .HasAnnotation("ProductVersion", "1.1.2")
18
+                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
19
+
20
+            modelBuilder.Entity("MAX.Models.Account", b =>
21
+                {
22
+                    b.Property<int>("Id");
23
+
24
+                    b.Property<decimal>("Balance");
25
+
26
+                    b.Property<string>("Name")
27
+                        .HasMaxLength(50);
28
+
29
+                    b.Property<string>("Reference")
30
+                        .HasMaxLength(50);
31
+
32
+                    b.Property<int>("Status");
33
+
34
+                    b.Property<int>("WarehouseId");
35
+
36
+                    b.HasKey("Id");
37
+
38
+                    b.HasIndex("WarehouseId");
39
+
40
+                    b.ToTable("Accounts");
41
+                });
42
+
43
+            modelBuilder.Entity("MAX.Models.User", b =>
44
+                {
45
+                    b.Property<int>("Id");
46
+
47
+                    b.Property<int>("AccountId");
48
+
49
+                    b.Property<bool>("CanPrintOffline");
50
+
51
+                    b.Property<bool>("CanPrintOnline");
52
+
53
+                    b.Property<bool>("CanReprintOffline");
54
+
55
+                    b.Property<bool>("CanReprintOnline");
56
+
57
+                    b.Property<bool>("Enabled");
58
+
59
+                    b.Property<string>("FirstName")
60
+                        .HasMaxLength(50);
61
+
62
+                    b.Property<DateTime>("LastLogin");
63
+
64
+                    b.Property<int>("Level");
65
+
66
+                    b.Property<decimal>("OfflinePrintValue");
67
+
68
+                    b.Property<decimal>("OfflineReprintValue");
69
+
70
+                    b.Property<decimal>("OnlinePrintValue");
71
+
72
+                    b.Property<decimal>("OnlineReprintValue");
73
+
74
+                    b.Property<string>("Surname")
75
+                        .HasMaxLength(50);
76
+
77
+                    b.Property<int>("System");
78
+
79
+                    b.Property<string>("Username")
80
+                        .HasMaxLength(50);
81
+
82
+                    b.HasKey("Id");
83
+
84
+                    b.HasIndex("AccountId");
85
+
86
+                    b.ToTable("Users");
87
+                });
88
+
89
+            modelBuilder.Entity("MAX.Models.Vendor", b =>
90
+                {
91
+                    b.Property<int>("Id");
92
+
93
+                    b.Property<int>("AccountId");
94
+
95
+                    b.Property<string>("SerialNumber")
96
+                        .HasMaxLength(50);
97
+
98
+                    b.HasKey("Id");
99
+
100
+                    b.HasIndex("AccountId");
101
+
102
+                    b.ToTable("Vendors");
103
+                });
104
+
105
+            modelBuilder.Entity("MAX.Models.Warehouse", b =>
106
+                {
107
+                    b.Property<int>("Id");
108
+
109
+                    b.Property<string>("Name")
110
+                        .HasMaxLength(50);
111
+
112
+                    b.HasKey("Id");
113
+
114
+                    b.ToTable("Warehouses");
115
+                });
116
+
117
+            modelBuilder.Entity("MAX.Models.Account", b =>
118
+                {
119
+                    b.HasOne("MAX.Models.Warehouse", "Warehouse")
120
+                        .WithMany("Accounts")
121
+                        .HasForeignKey("WarehouseId")
122
+                        .OnDelete(DeleteBehavior.Cascade);
123
+                });
124
+
125
+            modelBuilder.Entity("MAX.Models.User", b =>
126
+                {
127
+                    b.HasOne("MAX.Models.Account", "Account")
128
+                        .WithMany("Users")
129
+                        .HasForeignKey("AccountId")
130
+                        .OnDelete(DeleteBehavior.Cascade);
131
+                });
132
+
133
+            modelBuilder.Entity("MAX.Models.Vendor", b =>
134
+                {
135
+                    b.HasOne("MAX.Models.Account", "Account")
136
+                        .WithMany("Vendors")
137
+                        .HasForeignKey("AccountId")
138
+                        .OnDelete(DeleteBehavior.Cascade);
139
+                });
140
+        }
141
+    }
142
+}

+ 128 - 0
BulkPrintingAPI/Migrations/20170701135738_InitialCreate.cs

@@ -0,0 +1,128 @@
1
+using System;
2
+using System.Collections.Generic;
3
+using Microsoft.EntityFrameworkCore.Migrations;
4
+
5
+namespace BulkPrintingAPI.Migrations
6
+{
7
+    public partial class InitialCreate : Migration
8
+    {
9
+        protected override void Up(MigrationBuilder migrationBuilder)
10
+        {
11
+            migrationBuilder.CreateTable(
12
+                name: "Warehouses",
13
+                columns: table => new
14
+                {
15
+                    Id = table.Column<int>(nullable: false),
16
+                    Name = table.Column<string>(maxLength: 50, nullable: true)
17
+                },
18
+                constraints: table =>
19
+                {
20
+                    table.PrimaryKey("PK_Warehouses", x => x.Id);
21
+                });
22
+
23
+            migrationBuilder.CreateTable(
24
+                name: "Accounts",
25
+                columns: table => new
26
+                {
27
+                    Id = table.Column<int>(nullable: false),
28
+                    Balance = table.Column<decimal>(nullable: false),
29
+                    Name = table.Column<string>(maxLength: 50, nullable: true),
30
+                    Reference = table.Column<string>(maxLength: 50, nullable: true),
31
+                    Status = table.Column<int>(nullable: false),
32
+                    WarehouseId = table.Column<int>(nullable: false)
33
+                },
34
+                constraints: table =>
35
+                {
36
+                    table.PrimaryKey("PK_Accounts", x => x.Id);
37
+                    table.ForeignKey(
38
+                        name: "FK_Accounts_Warehouses_WarehouseId",
39
+                        column: x => x.WarehouseId,
40
+                        principalTable: "Warehouses",
41
+                        principalColumn: "Id",
42
+                        onDelete: ReferentialAction.Cascade);
43
+                });
44
+
45
+            migrationBuilder.CreateTable(
46
+                name: "Users",
47
+                columns: table => new
48
+                {
49
+                    Id = table.Column<int>(nullable: false),
50
+                    AccountId = table.Column<int>(nullable: false),
51
+                    CanPrintOffline = table.Column<bool>(nullable: false),
52
+                    CanPrintOnline = table.Column<bool>(nullable: false),
53
+                    CanReprintOffline = table.Column<bool>(nullable: false),
54
+                    CanReprintOnline = table.Column<bool>(nullable: false),
55
+                    Enabled = table.Column<bool>(nullable: false),
56
+                    FirstName = table.Column<string>(maxLength: 50, nullable: true),
57
+                    LastLogin = table.Column<DateTime>(nullable: false),
58
+                    Level = table.Column<int>(nullable: false),
59
+                    OfflinePrintValue = table.Column<decimal>(nullable: false),
60
+                    OfflineReprintValue = table.Column<decimal>(nullable: false),
61
+                    OnlinePrintValue = table.Column<decimal>(nullable: false),
62
+                    OnlineReprintValue = table.Column<decimal>(nullable: false),
63
+                    Surname = table.Column<string>(maxLength: 50, nullable: true),
64
+                    System = table.Column<int>(nullable: false),
65
+                    Username = table.Column<string>(maxLength: 50, nullable: true)
66
+                },
67
+                constraints: table =>
68
+                {
69
+                    table.PrimaryKey("PK_Users", x => x.Id);
70
+                    table.ForeignKey(
71
+                        name: "FK_Users_Accounts_AccountId",
72
+                        column: x => x.AccountId,
73
+                        principalTable: "Accounts",
74
+                        principalColumn: "Id",
75
+                        onDelete: ReferentialAction.Cascade);
76
+                });
77
+
78
+            migrationBuilder.CreateTable(
79
+                name: "Vendors",
80
+                columns: table => new
81
+                {
82
+                    Id = table.Column<int>(nullable: false),
83
+                    AccountId = table.Column<int>(nullable: false),
84
+                    SerialNumber = table.Column<string>(maxLength: 50, nullable: true)
85
+                },
86
+                constraints: table =>
87
+                {
88
+                    table.PrimaryKey("PK_Vendors", x => x.Id);
89
+                    table.ForeignKey(
90
+                        name: "FK_Vendors_Accounts_AccountId",
91
+                        column: x => x.AccountId,
92
+                        principalTable: "Accounts",
93
+                        principalColumn: "Id",
94
+                        onDelete: ReferentialAction.Cascade);
95
+                });
96
+
97
+            migrationBuilder.CreateIndex(
98
+                name: "IX_Accounts_WarehouseId",
99
+                table: "Accounts",
100
+                column: "WarehouseId");
101
+
102
+            migrationBuilder.CreateIndex(
103
+                name: "IX_Users_AccountId",
104
+                table: "Users",
105
+                column: "AccountId");
106
+
107
+            migrationBuilder.CreateIndex(
108
+                name: "IX_Vendors_AccountId",
109
+                table: "Vendors",
110
+                column: "AccountId");
111
+        }
112
+
113
+        protected override void Down(MigrationBuilder migrationBuilder)
114
+        {
115
+            migrationBuilder.DropTable(
116
+                name: "Users");
117
+
118
+            migrationBuilder.DropTable(
119
+                name: "Vendors");
120
+
121
+            migrationBuilder.DropTable(
122
+                name: "Accounts");
123
+
124
+            migrationBuilder.DropTable(
125
+                name: "Warehouses");
126
+        }
127
+    }
128
+}

+ 141 - 0
BulkPrintingAPI/Migrations/MAXDbContextModelSnapshot.cs

@@ -0,0 +1,141 @@
1
+using System;
2
+using Microsoft.EntityFrameworkCore;
3
+using Microsoft.EntityFrameworkCore.Infrastructure;
4
+using Microsoft.EntityFrameworkCore.Metadata;
5
+using Microsoft.EntityFrameworkCore.Migrations;
6
+using MAX.Models;
7
+
8
+namespace BulkPrintingAPI.Migrations
9
+{
10
+    [DbContext(typeof(MAXDbContext))]
11
+    partial class MAXDbContextModelSnapshot : ModelSnapshot
12
+    {
13
+        protected override void BuildModel(ModelBuilder modelBuilder)
14
+        {
15
+            modelBuilder
16
+                .HasAnnotation("ProductVersion", "1.1.2")
17
+                .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
18
+
19
+            modelBuilder.Entity("MAX.Models.Account", b =>
20
+                {
21
+                    b.Property<int>("Id");
22
+
23
+                    b.Property<decimal>("Balance");
24
+
25
+                    b.Property<string>("Name")
26
+                        .HasMaxLength(50);
27
+
28
+                    b.Property<string>("Reference")
29
+                        .HasMaxLength(50);
30
+
31
+                    b.Property<int>("Status");
32
+
33
+                    b.Property<int>("WarehouseId");
34
+
35
+                    b.HasKey("Id");
36
+
37
+                    b.HasIndex("WarehouseId");
38
+
39
+                    b.ToTable("Accounts");
40
+                });
41
+
42
+            modelBuilder.Entity("MAX.Models.User", b =>
43
+                {
44
+                    b.Property<int>("Id");
45
+
46
+                    b.Property<int>("AccountId");
47
+
48
+                    b.Property<bool>("CanPrintOffline");
49
+
50
+                    b.Property<bool>("CanPrintOnline");
51
+
52
+                    b.Property<bool>("CanReprintOffline");
53
+
54
+                    b.Property<bool>("CanReprintOnline");
55
+
56
+                    b.Property<bool>("Enabled");
57
+
58
+                    b.Property<string>("FirstName")
59
+                        .HasMaxLength(50);
60
+
61
+                    b.Property<DateTime>("LastLogin");
62
+
63
+                    b.Property<int>("Level");
64
+
65
+                    b.Property<decimal>("OfflinePrintValue");
66
+
67
+                    b.Property<decimal>("OfflineReprintValue");
68
+
69
+                    b.Property<decimal>("OnlinePrintValue");
70
+
71
+                    b.Property<decimal>("OnlineReprintValue");
72
+
73
+                    b.Property<string>("Surname")
74
+                        .HasMaxLength(50);
75
+
76
+                    b.Property<int>("System");
77
+
78
+                    b.Property<string>("Username")
79
+                        .HasMaxLength(50);
80
+
81
+                    b.HasKey("Id");
82
+
83
+                    b.HasIndex("AccountId");
84
+
85
+                    b.ToTable("Users");
86
+                });
87
+
88
+            modelBuilder.Entity("MAX.Models.Vendor", b =>
89
+                {
90
+                    b.Property<int>("Id");
91
+
92
+                    b.Property<int>("AccountId");
93
+
94
+                    b.Property<string>("SerialNumber")
95
+                        .HasMaxLength(50);
96
+
97
+                    b.HasKey("Id");
98
+
99
+                    b.HasIndex("AccountId");
100
+
101
+                    b.ToTable("Vendors");
102
+                });
103
+
104
+            modelBuilder.Entity("MAX.Models.Warehouse", b =>
105
+                {
106
+                    b.Property<int>("Id");
107
+
108
+                    b.Property<string>("Name")
109
+                        .HasMaxLength(50);
110
+
111
+                    b.HasKey("Id");
112
+
113
+                    b.ToTable("Warehouses");
114
+                });
115
+
116
+            modelBuilder.Entity("MAX.Models.Account", b =>
117
+                {
118
+                    b.HasOne("MAX.Models.Warehouse", "Warehouse")
119
+                        .WithMany("Accounts")
120
+                        .HasForeignKey("WarehouseId")
121
+                        .OnDelete(DeleteBehavior.Cascade);
122
+                });
123
+
124
+            modelBuilder.Entity("MAX.Models.User", b =>
125
+                {
126
+                    b.HasOne("MAX.Models.Account", "Account")
127
+                        .WithMany("Users")
128
+                        .HasForeignKey("AccountId")
129
+                        .OnDelete(DeleteBehavior.Cascade);
130
+                });
131
+
132
+            modelBuilder.Entity("MAX.Models.Vendor", b =>
133
+                {
134
+                    b.HasOne("MAX.Models.Account", "Account")
135
+                        .WithMany("Vendors")
136
+                        .HasForeignKey("AccountId")
137
+                        .OnDelete(DeleteBehavior.Cascade);
138
+                });
139
+        }
140
+    }
141
+}

+ 49 - 0
BulkPrintingAPI/ScaffoldingReadMe.txt

@@ -0,0 +1,49 @@
1
+
2
+ASP.NET MVC core dependencies have been added to the project.
3
+However you may still need to do make changes to your project.
4
+
5
+1. Suggested changes to Startup class:
6
+    1.1 Add a constructor:
7
+        public IConfigurationRoot Configuration { get; }
8
+
9
+        public Startup(IHostingEnvironment env)
10
+        {
11
+            var builder = new ConfigurationBuilder()
12
+                .SetBasePath(env.ContentRootPath)
13
+                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
14
+                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
15
+                .AddEnvironmentVariables();
16
+            Configuration = builder.Build();
17
+        }
18
+    1.2 Add MVC services:
19
+        public void ConfigureServices(IServiceCollection services)
20
+        {
21
+            // Add framework services.
22
+            services.AddMvc();
23
+       }
24
+
25
+    1.3 Configure web app to use use Configuration and use MVC routing:
26
+
27
+        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
28
+        {
29
+            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
30
+            loggerFactory.AddDebug();
31
+
32
+            if (env.IsDevelopment())
33
+            {
34
+                app.UseDeveloperExceptionPage();
35
+            }
36
+            else
37
+            {
38
+                app.UseExceptionHandler("/Home/Error");
39
+            }
40
+
41
+            app.UseStaticFiles();
42
+
43
+            app.UseMvc(routes =>
44
+            {
45
+                routes.MapRoute(
46
+                    name: "default",
47
+                    template: "{controller=Home}/{action=Index}/{id?}");
48
+            });
49
+        }

+ 34 - 0
BulkPrintingAPI/Services/MAXClientFactory.cs

@@ -0,0 +1,34 @@
1
+using Microsoft.Extensions.Configuration;
2
+using Microsoft.Extensions.Logging;
3
+
4
+namespace BulkPrintingAPI.Services
5
+{
6
+    public class MAXClientFactory
7
+    {
8
+        public MAXClientFactory(IConfiguration configuration)
9
+        {
10
+            configuration.Bind(this);
11
+        }
12
+
13
+        public MAX.Client GetClient(ILogger logger, int vendorId, string serialNumber, int userId,
14
+            string username, string password)
15
+        {
16
+            var client = new MAX.Client(logger, Host, Port, vendorId, serialNumber, userId,
17
+                username, password);
18
+            client.ConnectTimeout = ConnectTimeout;
19
+            client.ReceiveTimeout = ReceiveTimeout;
20
+            client.SendTimeout = SendTimeout;
21
+            return client;
22
+        }
23
+
24
+        public string Host { get; set; }
25
+        
26
+        public int Port { get; set; }
27
+
28
+        public int ConnectTimeout { get; set; }
29
+
30
+        public int ReceiveTimeout { get; set; }
31
+
32
+        public int SendTimeout { get; set; }
33
+    }
34
+}

+ 0 - 76
BulkPrintingAPI/Startup.Auth.cs

@@ -1,76 +0,0 @@
1
-using BulkPrintingAPI.Authentication;
2
-using Microsoft.AspNetCore.Builder;
3
-using Microsoft.Extensions.Options;
4
-using Microsoft.IdentityModel.Tokens;
5
-using System;
6
-using System.Security.Claims;
7
-using System.Security.Cryptography;
8
-using System.Security.Principal;
9
-using System.Text;
10
-using System.Threading.Tasks;
11
-
12
-namespace BulkPrintingAPI
13
-{
14
-    public partial class Startup
15
-    {
16
-        private void ConfigureAuth(IApplicationBuilder app)
17
-        {
18
-            var rawKey = new Rfc2898DeriveBytes(
19
-                    Encoding.ASCII.GetBytes(Configuration.GetSection("TokenAuthentication:SecretKey").Value),
20
-                    Encoding.ASCII.GetBytes(Configuration.GetSection("TokenAuthentication:Salt").Value),
21
-                    int.Parse(Configuration.GetSection("TokenAuthentication:Iterations").Value)
22
-                ).GetBytes(32);
23
-            var securityKey = new SymmetricSecurityKey(rawKey);
24
-
25
-            var tokenValidationParameters = new TokenValidationParameters
26
-            {
27
-                ValidateIssuerSigningKey = true,
28
-                IssuerSigningKey = securityKey,
29
-                TokenDecryptionKey = securityKey,
30
-                
31
-                ValidateIssuer = true,
32
-                ValidIssuer = Configuration.GetSection("TokenAuthentication:Issuer").Value,
33
-
34
-                ValidateAudience = true,
35
-                ValidAudience = Configuration.GetSection("TokenAuthentication:Audience").Value,
36
-
37
-                ValidateLifetime = true,
38
-                ClockSkew = TimeSpan.Zero
39
-            };
40
-
41
-            var bearerOptions = new JwtBearerOptions
42
-            {
43
-                AutomaticAuthenticate = true,
44
-                AutomaticChallenge = true,
45
-                SaveToken = true,
46
-                TokenValidationParameters = tokenValidationParameters
47
-            };
48
-
49
-            app.UseJwtBearerAuthentication(bearerOptions);
50
-
51
-            var tokenProviderOptions = new TokenProviderOptions
52
-            {
53
-                Path = Configuration.GetSection("TokenAuthentication:TokenPath").Value,
54
-                Audience = Configuration.GetSection("TokenAuthentication:Audience").Value,
55
-                Issuer = Configuration.GetSection("TokenAuthentication:Issuer").Value,
56
-                SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256),
57
-                EncryptingCredentials = new EncryptingCredentials(securityKey, "dir", SecurityAlgorithms.Aes128CbcHmacSha256),
58
-                IdentityResolver = GetIdentity
59
-            };
60
-
61
-            app.UseMiddleware<TokenProviderMiddleware>(Options.Create(tokenProviderOptions));
62
-        }
63
-
64
-        private Task<ClaimsIdentity> GetIdentity(string username, string password)
65
-        {
66
-            // Don't do this in production, obviously!
67
-            if (username == "TEST" && password == "TEST123")
68
-            {
69
-                return Task.FromResult(new ClaimsIdentity(new GenericIdentity(username, "Token"), new Claim[] { }));
70
-            }
71
-
72
-            // Credentials are invalid, or account doesn't exist
73
-            return Task.FromResult<ClaimsIdentity>(null);
74
-        }
75
-    }
76
-}

+ 48 - 5
BulkPrintingAPI/Startup.cs

@@ -1,12 +1,18 @@
1
-using Microsoft.AspNetCore.Builder;
1
+using BulkPrintingAPI.Configuration;
2
+using Microsoft.AspNetCore.Authorization;
3
+using Microsoft.AspNetCore.Builder;
2 4
 using Microsoft.AspNetCore.Hosting;
5
+using Microsoft.AspNetCore.Mvc.Authorization;
6
+using Microsoft.EntityFrameworkCore;
3 7
 using Microsoft.Extensions.Configuration;
4 8
 using Microsoft.Extensions.DependencyInjection;
5 9
 using Microsoft.Extensions.Logging;
10
+using Microsoft.IdentityModel.Tokens;
11
+using System;
6 12
 
7 13
 namespace BulkPrintingAPI
8 14
 {
9
-    public partial class Startup
15
+    public class Startup
10 16
     {
11 17
         public Startup(IHostingEnvironment env)
12 18
         {
@@ -26,16 +32,53 @@ namespace BulkPrintingAPI
26 32
         public void ConfigureServices(IServiceCollection services)
27 33
         {
28 34
             // Add framework services.
29
-            services.AddMvc();
35
+            services.AddMvc(config =>
36
+            {
37
+                var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
38
+                config.Filters.Add(new AuthorizeFilter(policy));
39
+            });
40
+            services.AddSingleton(new TokenAuthenticationOptions(
41
+                Configuration.GetSection("TokenAuthentication")));
42
+            services.AddSingleton(new Services.MAXClientFactory(
43
+                Configuration.GetSection("MAX")));
44
+            services.AddDbContext<MAX.Models.MAXDbContext>(
45
+                options => options.UseSqlServer(
46
+                    Configuration["Database:ConnectionString"],
47
+                    b => b.MigrationsAssembly("BulkPrintingAPI")));
30 48
         }
31 49
 
32 50
         // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
33
-        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
51
+        public void Configure(IApplicationBuilder app, IHostingEnvironment env,
52
+            ILoggerFactory loggerFactory, TokenAuthenticationOptions tokenAuthenticationOptions)
34 53
         {
35 54
             loggerFactory.AddConsole(Configuration.GetSection("Logging"));
36 55
             loggerFactory.AddDebug();
37 56
 
38
-            ConfigureAuth(app);
57
+            var tokenValidationParameters = new TokenValidationParameters
58
+            {
59
+                ValidateIssuerSigningKey = true,
60
+                IssuerSigningKey = tokenAuthenticationOptions.Key,
61
+                TokenDecryptionKey = tokenAuthenticationOptions.Key,
62
+
63
+                ValidateIssuer = true,
64
+                ValidIssuer = tokenAuthenticationOptions.Issuer,
65
+
66
+                ValidateAudience = true,
67
+                ValidAudience = tokenAuthenticationOptions.Audience,
68
+
69
+                ValidateLifetime = true,
70
+                ClockSkew = TimeSpan.Zero
71
+            };
72
+
73
+            var bearerOptions = new JwtBearerOptions
74
+            {
75
+                AutomaticAuthenticate = true,
76
+                AutomaticChallenge = true,
77
+                SaveToken = true,
78
+                TokenValidationParameters = tokenValidationParameters
79
+            };
80
+
81
+            app.UseJwtBearerAuthentication(bearerOptions);
39 82
 
40 83
             app.UseMvc();
41 84
         }

+ 7 - 0
BulkPrintingAPI/appsettings.Development.json

@@ -6,5 +6,12 @@
6 6
       "System": "Information",
7 7
       "Microsoft": "Information"
8 8
     }
9
+  },
10
+  "MAX": {
11
+    "Host": "41.223.25.5",
12
+    "Port": 4006,
13
+    "ConnectTimeout": 10000,
14
+    "ReceiveTimeout": 10000,
15
+    "SendTimeout":  10000
9 16
   }
10 17
 }

+ 4 - 3
BulkPrintingAPI/appsettings.json

@@ -1,4 +1,7 @@
1 1
 {
2
+  "Database": {
3
+    "ConnectionString": "SET IN secrets.json"
4
+  },
2 5
   "Logging": {
3 6
     "IncludeScopes": false,
4 7
     "LogLevel": {
@@ -10,8 +13,6 @@
10 13
     "Issuer": "bulk",
11 14
     "SecretKey": "SET IN secrets.json",
12 15
     "Salt": "SET IN secrets.json",
13
-    "Iterations": 4096,
14
-    "TokenPath": "/token",
15
-    "CookieName": "access_token"
16
+    "TokenLifetime": 86400
16 17
   }
17 18
 }

+ 328 - 0
MAXClient/Client.cs

@@ -0,0 +1,328 @@
1
+using ExtensionMethods;
2
+using MAX.Models;
3
+using Microsoft.Extensions.Logging;
4
+using System;
5
+using System.IO;
6
+using System.Net.Sockets;
7
+using System.Security.Cryptography;
8
+using System.Text;
9
+using System.Threading;
10
+using System.Threading.Tasks;
11
+using System.Xml;
12
+
13
+namespace MAX
14
+{
15
+    public class Client : IDisposable
16
+    {
17
+        private ILogger logger;
18
+        private string host;
19
+        private int port;
20
+        private int vendorId;
21
+        private string serialNumber;
22
+        private int userId;
23
+        private string username;
24
+        private string password;
25
+
26
+        private TcpClient connection = null;
27
+        private NetworkStream connectionStream = null;
28
+        private TripleDES des = null;
29
+
30
+        private bool disposed = false;
31
+
32
+        public Client(ILogger logger, string host, int port, int vendorId, string serialNumber, int userId, string username, string password)
33
+        {
34
+            this.logger = logger;
35
+            this.host = host;
36
+            this.port = port;
37
+            this.vendorId = vendorId;
38
+            this.serialNumber = serialNumber;
39
+            this.userId = userId;
40
+            this.username = username;
41
+            this.password = password;
42
+
43
+            ConnectTimeout = 10000;
44
+            ReceiveTimeout = 10000;
45
+            SendTimeout = 10000;
46
+        }
47
+
48
+        public void Close()
49
+        {
50
+            Dispose(true);
51
+        }
52
+
53
+        public async Task<User> ConnectAsync()
54
+        {
55
+            if (connection != null)
56
+                throw new Exception("Already connected");
57
+
58
+            connection = new TcpClient(AddressFamily.InterNetwork);
59
+            connection.ReceiveTimeout = ReceiveTimeout;
60
+            connection.SendTimeout = SendTimeout;
61
+
62
+            // Connect to the server
63
+            try
64
+            {
65
+                using (var cancellationSource = new CancellationTokenSource(ConnectTimeout))
66
+                {
67
+                    await connection.ConnectAsync(host, port).WithCancellation(cancellationSource.Token).ConfigureAwait(false);
68
+                }
69
+            }
70
+            catch (OperationCanceledException)
71
+            {
72
+                throw new Exception("Connect timeout");
73
+            }
74
+            connectionStream = connection.GetStream();
75
+
76
+            // Device authentication
77
+            await WriteMessageAsync(new MessageBuilder()
78
+                .Append("Hi ")
79
+                .Append(serialNumber)
80
+                .Append("|V")
81
+                .Append(vendorId)
82
+                .Append("|123451234512345||||||")).ConfigureAwait(false);
83
+
84
+            var response = await ReadMessageAsync().ConfigureAwait(false);
85
+            if (!response.StartsWith("Hi "))
86
+            {
87
+                logger.LogError("Device authentication failed: {0}", response);
88
+                return null;
89
+            }
90
+
91
+            // Request server RSA key
92
+            await WriteMessageAsync(new MessageBuilder().Append("PK")).ConfigureAwait(false);
93
+            response = await ReadMessageAsync().ConfigureAwait(false);
94
+
95
+            // Key exchange
96
+            des = TripleDES.Create();
97
+            des.IV = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
98
+            await WriteMessageAsync(new MessageBuilder()
99
+                .Append("3D ")
100
+                .Append(EncryptRSA(response, BitConverter.ToString(des.Key).Replace("-", "")))).ConfigureAwait(false);
101
+
102
+            response = await ReadMessageAsync().ConfigureAwait(false);
103
+            if (!response.StartsWith("OK"))
104
+            {
105
+                throw new Exception(String.Format("Key exchange failed: {0}", response));
106
+            }
107
+
108
+            // User authentication
109
+            await WriteMessageAsync(new MessageBuilder()
110
+                .Append("User ")
111
+                .Append(Encrypt(new StringBuilder()
112
+                    .Append(userId)
113
+                    .Append("|")
114
+                    .Append(username)
115
+                    .Append("|")
116
+                    .Append(password).ToString()))).ConfigureAwait(false);
117
+
118
+            response = Decrypt(await ReadMessageAsync().ConfigureAwait(false));
119
+            if (response.StartsWith("OK"))
120
+            {
121
+                var parts = response.Split('|');
122
+                var user = new User()
123
+                {
124
+                    Id = userId,
125
+                    Username = username,
126
+                    FirstName = parts[4],
127
+                    Surname = parts[3],
128
+                    Enabled = bool.Parse(parts[6]),
129
+                    Level = (User.UserLevel)int.Parse(parts[1]),
130
+                    System = int.Parse(parts[2]),
131
+                    LastLogin = DateTime.Parse(parts[5])
132
+                };
133
+
134
+                if (user.Level == User.UserLevel.CustomUser)
135
+                {
136
+                    user.CanPrintOffline = bool.Parse(parts[7]);
137
+                    user.OfflinePrintValue = decimal.Parse(parts[8]);
138
+                    user.CanPrintOnline = bool.Parse(parts[9]);
139
+                    user.OnlinePrintValue = decimal.Parse(parts[10]);
140
+                    user.CanReprintOffline = bool.Parse(parts[11]);
141
+                    user.OfflineReprintValue = decimal.Parse(parts[12]);
142
+                    user.CanReprintOnline = bool.Parse(parts[13]);
143
+                    user.OnlineReprintValue = decimal.Parse(parts[14]);
144
+                }
145
+
146
+                // Account information
147
+                await WriteMessageAsync(new MessageBuilder().Append("Acc")).ConfigureAwait(false);
148
+                response = Decrypt(await ReadMessageAsync().ConfigureAwait(false));
149
+                if (response.StartsWith("OK"))
150
+                {
151
+                    parts = response.Split('|');
152
+                    user.Account = new Account()
153
+                    {
154
+                        Id = int.Parse(parts[1]),
155
+                        Name = parts[2],
156
+                        Balance = decimal.Parse(parts[3]),
157
+                        Status = (Account.AccountStatus)int.Parse(parts[4]),
158
+                        Reference = parts[5],
159
+                        Warehouse = new Warehouse()
160
+                        {
161
+                            Id = int.Parse(parts[6]),
162
+                            Name = parts[7]
163
+                        }
164
+                    };
165
+                    return user;
166
+                }
167
+                else if (response.StartsWith("ER"))
168
+                {
169
+                    logger.LogError("Error retrieving account information: {0}", response);
170
+                    return null;
171
+                }
172
+                else
173
+                {
174
+                    throw new Exception(String.Format("Invalid account information response: {0}", response));
175
+                }
176
+            }
177
+            else if (response.StartsWith("ER"))
178
+            {
179
+                logger.LogInformation("User authentication failed: {0}", response);
180
+                return null;
181
+            }
182
+            else
183
+            {
184
+                throw new Exception(String.Format("Invalid user information response: {0}", response));
185
+            }
186
+        }
187
+
188
+        public int ConnectTimeout { get; set; }
189
+
190
+        protected virtual void Dispose(bool disposing)
191
+        {
192
+            if (disposed)
193
+                return;
194
+
195
+            disposed = true;
196
+
197
+            // No unmanaged resources are disposed so we don't need the full finalisation pattern.
198
+            if (disposing)
199
+            {
200
+                if (des != null)
201
+                {
202
+                    des.Dispose();
203
+                    des = null;
204
+                }
205
+                if (connectionStream != null)
206
+                {
207
+                    connectionStream.Dispose();
208
+                    connectionStream = null;
209
+                }
210
+                if (connection != null)
211
+                {
212
+                    connection.Dispose();
213
+                    connection = null;
214
+                }
215
+            }
216
+        }
217
+
218
+        public void Dispose()
219
+        {
220
+            Dispose(true);
221
+        }
222
+
223
+        private string Decrypt(string cipherText)
224
+        {
225
+            using (var decryptor = des.CreateDecryptor(des.Key, des.IV))
226
+            {
227
+                return Encoding.UTF8.GetString(Transform(decryptor, Convert.FromBase64String(cipherText)));
228
+            }
229
+        }
230
+
231
+        private string Encrypt(string plainText)
232
+        {
233
+            using (var encryptor = des.CreateEncryptor(des.Key, des.IV))
234
+            {
235
+                return Convert.ToBase64String(Transform(encryptor, Encoding.UTF8.GetBytes(plainText)));
236
+            }
237
+        }
238
+
239
+        private string EncryptRSA(string publicKey, string plainText)
240
+        {
241
+            RSAParameters parameters = new RSAParameters();
242
+
243
+            var xml = new XmlDocument();
244
+            xml.LoadXml(publicKey);
245
+            if (! xml.DocumentElement.Name.Equals("RSAKeyValue"))
246
+                throw new Exception("Invalid RSA key");
247
+
248
+            foreach (XmlNode node in xml.DocumentElement.ChildNodes)
249
+            {
250
+                switch (node.Name)
251
+                {
252
+                    case "Modulus": parameters.Modulus = Convert.FromBase64String(node.InnerText); break;
253
+                    case "Exponent": parameters.Exponent = Convert.FromBase64String(node.InnerText); break;
254
+                    case "P": parameters.P = Convert.FromBase64String(node.InnerText); break;
255
+                    case "Q": parameters.Q = Convert.FromBase64String(node.InnerText); break;
256
+                    case "DP": parameters.DP = Convert.FromBase64String(node.InnerText); break;
257
+                    case "DQ": parameters.DQ = Convert.FromBase64String(node.InnerText); break;
258
+                    case "InverseQ": parameters.InverseQ = Convert.FromBase64String(node.InnerText); break;
259
+                    case "D": parameters.D = Convert.FromBase64String(node.InnerText); break;
260
+                }
261
+            }
262
+
263
+            using (var rsa = RSA.Create())
264
+            {
265
+                rsa.ImportParameters(parameters);
266
+                var blockSize = rsa.KeySize / 8 - 42;
267
+                var offset = 0;
268
+                var input = Encoding.UTF32.GetBytes(plainText);
269
+                StringBuilder output = new StringBuilder();
270
+                while (offset < input.Length)
271
+                {
272
+                    var length = input.Length - offset;
273
+                    if (length > blockSize)
274
+                        length = blockSize;
275
+                    var block = new byte[length];
276
+                    Array.Copy(input, offset, block, 0, length);
277
+                    var cipherText = rsa.Encrypt(block, RSAEncryptionPadding.OaepSHA1);
278
+                    Array.Reverse(cipherText);
279
+                    output.Append(Convert.ToBase64String(cipherText));
280
+                    offset += length;
281
+                }
282
+                return output.ToString();
283
+            }
284
+        }
285
+
286
+        private async Task<byte[]> ReadBytesAsync(int count)
287
+        {
288
+            int totalBytesRead = 0;
289
+            byte[] buffer = new byte[count];
290
+            while (totalBytesRead < count)
291
+            {
292
+                int bytesRead = await connectionStream.ReadAsync(buffer, totalBytesRead, count - totalBytesRead).ConfigureAwait(false);
293
+                if (bytesRead == 0)
294
+                    throw new Exception("Connection closed unexpectedly");
295
+                totalBytesRead += bytesRead;
296
+            }
297
+            return buffer;
298
+        }
299
+
300
+        private async Task<string> ReadMessageAsync()
301
+        {
302
+            byte[] buffer = await ReadBytesAsync(2).ConfigureAwait(false);
303
+            int size = buffer[0] * 256 + buffer[1];
304
+            return Encoding.ASCII.GetString(await ReadBytesAsync(size).ConfigureAwait(false));
305
+        }
306
+
307
+        public int ReceiveTimeout { get; set; }
308
+
309
+        public int SendTimeout { get; set; }
310
+
311
+        private byte[] Transform(ICryptoTransform transform, byte[] input)
312
+        {
313
+            using (var memoryStream = new MemoryStream())
314
+            using (var cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write))
315
+            {
316
+                cryptoStream.Write(input, 0, input.Length);
317
+                cryptoStream.FlushFinalBlock();
318
+                return memoryStream.ToArray();
319
+            }
320
+        }
321
+
322
+        private async Task WriteMessageAsync(MessageBuilder message)
323
+        {
324
+            byte[] data = message.GetBytes();
325
+            await connectionStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
326
+        }
327
+    }
328
+}

+ 22 - 0
MAXClient/ExtensionMethods.cs

@@ -0,0 +1,22 @@
1
+using System;
2
+using System.Threading;
3
+using System.Threading.Tasks;
4
+
5
+namespace ExtensionMethods
6
+{
7
+    public static class ExtensionMethods
8
+    {
9
+        public static async Task WithCancellation(this Task task,
10
+            CancellationToken cancellationToken)
11
+        {
12
+            var completionSource = new TaskCompletionSource<bool>();
13
+            using (cancellationToken.Register(
14
+                s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
15
+                completionSource))
16
+            {
17
+                if (task != await Task.WhenAny(task, completionSource.Task).ConfigureAwait(false))
18
+                    throw new OperationCanceledException(cancellationToken);
19
+            }
20
+        }
21
+    }
22
+}

+ 16 - 0
MAXClient/MAXClient.csproj

@@ -0,0 +1,16 @@
1
+<Project Sdk="Microsoft.NET.Sdk">
2
+
3
+  <PropertyGroup>
4
+    <TargetFramework>netcoreapp1.1</TargetFramework>
5
+  </PropertyGroup>
6
+
7
+  <ItemGroup>
8
+    <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.2" />
9
+    <PackageReference Include="System.Xml.XmlDocument" Version="4.3.0" />
10
+  </ItemGroup>
11
+
12
+  <ItemGroup>
13
+    <ProjectReference Include="..\MAXData\MAXModels.csproj" />
14
+  </ItemGroup>
15
+
16
+</Project>

+ 34 - 0
MAXClient/MessageBuilder.cs

@@ -0,0 +1,34 @@
1
+using System;
2
+using System.Text;
3
+
4
+namespace MAX
5
+{
6
+    public class MessageBuilder
7
+    {
8
+        private StringBuilder builder;
9
+
10
+        public MessageBuilder()
11
+        {
12
+            builder = new StringBuilder(1024);
13
+            builder.Append("\0\0");
14
+        }
15
+
16
+        public MessageBuilder Append<T>(T value)
17
+        {
18
+            builder.Append(value);
19
+            return this;
20
+        }
21
+
22
+        public byte[] GetBytes()
23
+        {
24
+            int length = builder.Length - 2;
25
+            if (length <= 0)
26
+                throw new Exception("Message is too short");
27
+            else if (length > 65535)
28
+                throw new Exception("Message is too long");
29
+            builder[0] = (char)(length / 256);
30
+            builder[1] = (char)(length % 256);
31
+            return Encoding.ASCII.GetBytes(builder.ToString());
32
+        }
33
+    }
34
+}

+ 11 - 0
MAXData/MAXModels.csproj

@@ -0,0 +1,11 @@
1
+<Project Sdk="Microsoft.NET.Sdk">
2
+
3
+  <PropertyGroup>
4
+    <TargetFramework>netcoreapp1.1</TargetFramework>
5
+  </PropertyGroup>
6
+
7
+  <ItemGroup>
8
+    <PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" />
9
+  </ItemGroup>
10
+
11
+</Project>

+ 37 - 0
MAXData/Models/Account.cs

@@ -0,0 +1,37 @@
1
+using System.Collections.Generic;
2
+using System.ComponentModel.DataAnnotations;
3
+using System.ComponentModel.DataAnnotations.Schema;
4
+
5
+namespace MAX.Models
6
+{
7
+    public class Account
8
+    {
9
+        public enum AccountStatus
10
+        {
11
+            Unknown,
12
+            Enabled,
13
+            Suspended,
14
+            Closed
15
+        }
16
+
17
+        [DatabaseGenerated(DatabaseGeneratedOption.None)]
18
+        public int Id { get; set; }
19
+
20
+        [MaxLength(50)]
21
+        public string Name { get; set; }
22
+
23
+        public AccountStatus Status { get; set; }
24
+
25
+        [MaxLength(50)]
26
+        public string Reference { get; set; }
27
+
28
+        public decimal Balance { get; set; }
29
+
30
+        public int WarehouseId { get; set; }
31
+        public Warehouse Warehouse { get; set; }
32
+
33
+        public ICollection<User> Users { get; set; }
34
+
35
+        public ICollection<Vendor> Vendors { get; set; }
36
+    }
37
+}

+ 14 - 0
MAXData/Models/MAXDbContext.cs

@@ -0,0 +1,14 @@
1
+using Microsoft.EntityFrameworkCore;
2
+
3
+namespace MAX.Models
4
+{
5
+    public class MAXDbContext : DbContext
6
+    {
7
+        public MAXDbContext(DbContextOptions<MAXDbContext> options) : base(options) { }
8
+
9
+        public DbSet<Account> Accounts { get; set;  }
10
+        public DbSet<User> Users { get; set; }
11
+        public DbSet<Vendor> Vendors { get; set; }
12
+        public DbSet<Warehouse> Warehouses { get; set;  }
13
+    }
14
+}

+ 55 - 0
MAXData/Models/User.cs

@@ -0,0 +1,55 @@
1
+using System;
2
+using System.ComponentModel.DataAnnotations;
3
+using System.ComponentModel.DataAnnotations.Schema;
4
+
5
+namespace MAX.Models
6
+{
7
+    public class User
8
+    {
9
+        public enum UserLevel
10
+        {
11
+            Unknown = 0,
12
+            Administrator = 4,
13
+            CustomUser = 6
14
+        }
15
+
16
+        [DatabaseGenerated(DatabaseGeneratedOption.None)]
17
+        public int Id { get; set; }
18
+
19
+        [MaxLength(50)]
20
+        public string Username { get; set; }
21
+
22
+        [MaxLength(50)]
23
+        public string FirstName { get; set; }
24
+
25
+        [MaxLength(50)]
26
+        public string Surname { get; set; }
27
+
28
+        public int AccountId { get; set; }
29
+        public Account Account { get; set; }
30
+
31
+        public bool Enabled { get; set; }
32
+
33
+        public UserLevel Level { get; set; }
34
+
35
+        public int System { get; set; }
36
+
37
+        public DateTime LastLogin { get; set; }
38
+
39
+        public bool CanPrintOnline { get; set; }
40
+
41
+        public decimal OnlinePrintValue { get; set; }
42
+
43
+        public bool CanReprintOnline { get; set; }
44
+
45
+        public decimal OnlineReprintValue { get; set; }
46
+
47
+        public bool CanPrintOffline { get; set; }
48
+
49
+        public decimal OfflinePrintValue { get; set; }
50
+
51
+        public bool CanReprintOffline { get; set; }
52
+
53
+        public decimal OfflineReprintValue { get; set; }
54
+    }
55
+}

+ 17 - 0
MAXData/Models/Vendor.cs

@@ -0,0 +1,17 @@
1
+using System.ComponentModel.DataAnnotations;
2
+using System.ComponentModel.DataAnnotations.Schema;
3
+
4
+namespace MAX.Models
5
+{
6
+    public class Vendor
7
+    {
8
+        [DatabaseGenerated(DatabaseGeneratedOption.None)]
9
+        public int Id { get; set; }
10
+
11
+        [MaxLength(50)]
12
+        public string SerialNumber { get; set; }
13
+
14
+        public int AccountId { get; set; }
15
+        public Account Account { get; set; }
16
+    }
17
+}

+ 17 - 0
MAXData/Models/Warehouse.cs

@@ -0,0 +1,17 @@
1
+using System.Collections.Generic;
2
+using System.ComponentModel.DataAnnotations;
3
+using System.ComponentModel.DataAnnotations.Schema;
4
+
5
+namespace MAX.Models
6
+{
7
+    public class Warehouse
8
+    {
9
+        [DatabaseGenerated(DatabaseGeneratedOption.None)]
10
+        public int Id { get; set; }
11
+
12
+        [MaxLength(50)]
13
+        public string Name { get; set; }
14
+
15
+        public ICollection<Account> Accounts { get; set; }
16
+    }
17
+}