Pārlūkot izejas kodu

Add support for products, batches and vouchers

Andrew Klopper 8 gadi atpakaļ
vecāks
revīzija
4240c0697a
35 mainītis faili ar 1692 papildinājumiem un 377 dzēšanām
  1. 1 2
      BulkPrintingAPI/BulkPrintingAPI.csproj
  2. 10 0
      BulkPrintingAPI/CacheKeys.cs
  3. 59 0
      BulkPrintingAPI/Configuration/DataEncryptionOptions.cs
  4. 2 16
      BulkPrintingAPI/Configuration/TokenAuthenticationOptions.cs
  5. 129 0
      BulkPrintingAPI/Controllers/BatchesController.cs
  6. 109 75
      BulkPrintingAPI/Controllers/LoginController.cs
  7. 37 0
      BulkPrintingAPI/Controllers/ProductsController.cs
  8. 353 0
      BulkPrintingAPI/Controllers/Utils.cs
  9. 0 42
      BulkPrintingAPI/Controllers/ValuesController.cs
  10. 68 0
      BulkPrintingAPI/Controllers/VouchersController.cs
  11. 0 84
      BulkPrintingAPI/Db/DbHelpers.cs
  12. 111 2
      BulkPrintingAPI/Migrations/20170701135738_InitialCreate.Designer.cs
  13. 86 7
      BulkPrintingAPI/Migrations/20170701135738_InitialCreate.cs
  14. 111 2
      BulkPrintingAPI/Migrations/MAXDbContextModelSnapshot.cs
  15. 3 3
      BulkPrintingAPI/Properties/launchSettings.json
  16. 5 5
      BulkPrintingAPI/Startup.cs
  17. 204 101
      MAXClient/Client.cs
  18. 16 7
      BulkPrintingAPI/Services/MAXClientFactory.cs
  19. 24 0
      MAXClient/LoginCredentials.cs
  20. 3 0
      MAXClient/MAXClient.csproj
  21. 8 8
      MAXClient/MessageBuilder.cs
  22. 13 0
      MAXClient/Models/Network.cs
  23. 9 0
      MAXClient/Models/OrderResponse.cs
  24. 20 0
      MAXClient/Models/Product.cs
  25. 42 0
      MAXClient/Models/ProductCatalogue.cs
  26. 108 0
      MAXClient/Utils.cs
  27. 1 0
      MAXData/MAXModels.csproj
  28. 4 4
      MAXData/Models/Account.cs
  29. 73 0
      MAXData/Models/Batch.cs
  30. 34 0
      MAXData/Models/MAXContext.cs
  31. 0 14
      MAXData/Models/MAXDbContext.cs
  32. 6 3
      MAXData/Models/User.cs
  33. 12 1
      MAXData/Models/Vendor.cs
  34. 30 0
      MAXData/Models/Voucher.cs
  35. 1 1
      MAXData/Models/Warehouse.cs

+ 1 - 2
BulkPrintingAPI/BulkPrintingAPI.csproj

@@ -6,10 +6,10 @@
6 6
   </PropertyGroup>
7 7
 
8 8
   <ItemGroup>
9
-    <Folder Include="Models\" />
10 9
     <Folder Include="wwwroot\" />
11 10
   </ItemGroup>
12 11
   <ItemGroup>
12
+    <PackageReference Include="CoreFtp" Version="1.3.5" />
13 13
     <PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.0.0" />
14 14
     <PackageReference Include="Microsoft.AspNetCore" Version="1.1.2" />
15 15
     <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.2" />
@@ -21,7 +21,6 @@
21 21
     <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer.Design" Version="1.1.2" />
22 22
     <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="1.1.1" />
23 23
     <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="1.1.2" />
24
-    <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="1.1.2" />
25 24
     <PackageReference Include="Microsoft.VisualStudio.Web.BrowserLink" Version="1.1.2" />
26 25
     <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="1.1.1" />
27 26
   </ItemGroup>

+ 10 - 0
BulkPrintingAPI/CacheKeys.cs

@@ -0,0 +1,10 @@
1
+namespace BulkPrintingAPI
2
+{
3
+    public static class CacheKeys
4
+    {
5
+        public static string GetAccountProductsKey(int accountId)
6
+        {
7
+            return string.Format("AccountProducts:{0}", accountId);
8
+        }
9
+    }
10
+}

+ 59 - 0
BulkPrintingAPI/Configuration/DataEncryptionOptions.cs

@@ -0,0 +1,59 @@
1
+using Microsoft.Extensions.Configuration;
2
+using System;
3
+using System.Security.Cryptography;
4
+
5
+namespace BulkPrintingAPI.Configuration
6
+{
7
+    public class DataEncryptionOptions
8
+    {
9
+        private const int _defaultIterations = 4096;
10
+        private const int _defaultSaltLength = 20;
11
+        private const string _defaultPasswordNoise = "shie5heeX6pekaehovuS2yu0Ciejah7a";
12
+
13
+        public DataEncryptionOptions(IConfiguration configuration)
14
+        {
15
+            DefaultKey = DeriveKey(configuration, "DefaultKey").GetBytes(32);
16
+            DefaultIterations = configuration.GetValue("DefaultIterations", _defaultIterations);
17
+            DefaultSaltLength = configuration.GetValue("DefaultSaltLength", _defaultSaltLength);
18
+            PasswordNoise = configuration.GetValue("PasswordFuzz", _defaultPasswordNoise);
19
+        }
20
+
21
+        public static Rfc2898DeriveBytes DeriveKey(IConfiguration root, string section)
22
+        {
23
+            var configuration = root.GetSection(section);
24
+
25
+            string password = configuration["Password"];
26
+            if (string.IsNullOrEmpty(password))
27
+                throw new ArgumentException(
28
+                    string.Format("Missing '{0}:Password' value", section),
29
+                    nameof(configuration));
30
+
31
+            string encodedSalt = configuration["EncodedSalt"];
32
+            if (string.IsNullOrEmpty(encodedSalt))
33
+                throw new ArgumentException(
34
+                    string.Format("Missing '{0}:EncodedSalt' value", section),
35
+                    nameof(configuration));
36
+            var salt = Convert.FromBase64String(encodedSalt);
37
+
38
+            return DeriveKey(password, salt, configuration.GetValue("Iterations", _defaultIterations));
39
+        }
40
+
41
+        public static Rfc2898DeriveBytes DeriveKey(string password, byte[] salt, int iterations)
42
+        {
43
+            return new Rfc2898DeriveBytes(password, salt, iterations);
44
+        }
45
+
46
+        public static Rfc2898DeriveBytes DeriveKey(string password, int saltSize, int iterations)
47
+        {
48
+            return new Rfc2898DeriveBytes(password, saltSize, iterations);
49
+        }
50
+
51
+        public byte[] DefaultKey { get; set; }
52
+
53
+        public int DefaultIterations { get; set; }
54
+
55
+        public int DefaultSaltLength { get; set; }
56
+
57
+        public string PasswordNoise { get; set; }
58
+    }
59
+}

+ 2 - 16
BulkPrintingAPI/Configuration/TokenAuthenticationOptions.cs

@@ -1,8 +1,6 @@
1 1
 using Microsoft.Extensions.Configuration;
2 2
 using Microsoft.IdentityModel.Tokens;
3 3
 using System;
4
-using System.Security.Cryptography;
5
-using System.Text;
6 4
 using System.Threading.Tasks;
7 5
 
8 6
 namespace BulkPrintingAPI.Configuration
@@ -11,23 +9,11 @@ namespace BulkPrintingAPI.Configuration
11 9
     {
12 10
         public TokenAuthenticationOptions(IConfiguration configuration)
13 11
         {
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 12
             Issuer = configuration.GetValue("Issuer", "bulk");
28 13
             Audience = configuration.GetValue("Audience", "bulk");
29 14
             Lifetime = TimeSpan.FromSeconds(configuration.GetValue("TokenLifetime", 600));
30
-            Key = new SymmetricSecurityKey(rawKey);
15
+            Key = new SymmetricSecurityKey(DataEncryptionOptions.DeriveKey(configuration, "Key")
16
+                .GetBytes(32));
31 17
             SigningCredentials = new SigningCredentials(Key, SecurityAlgorithms.HmacSha256);
32 18
             EncryptingCredentials = new EncryptingCredentials(Key, "dir",
33 19
                 SecurityAlgorithms.Aes128CbcHmacSha256);

+ 129 - 0
BulkPrintingAPI/Controllers/BatchesController.cs

@@ -0,0 +1,129 @@
1
+using BulkPrintingAPI.Configuration;
2
+using MAX.Models;
3
+using Microsoft.AspNetCore.Mvc;
4
+using Microsoft.EntityFrameworkCore;
5
+using Microsoft.Extensions.Caching.Memory;
6
+using Microsoft.Extensions.Logging;
7
+using System;
8
+using System.Collections.Generic;
9
+using System.ComponentModel.DataAnnotations;
10
+using System.Linq;
11
+using System.Threading.Tasks;
12
+
13
+namespace BulkPrintingAPI.Controllers
14
+{
15
+    [Produces("application/json")]
16
+    [Route("api/v1/[controller]")]
17
+    public class BatchesController : Controller
18
+    {
19
+        public class OrderRequest
20
+        {
21
+            [Required]
22
+            public int? ProductId { get; set; }
23
+
24
+            [Required]
25
+            [Range(1, int.MaxValue)]
26
+            public int Quantity { get; set; }
27
+
28
+            [Required]
29
+            public string CustomerReference { get; set; }
30
+        };
31
+
32
+        private readonly ILogger _logger;
33
+        private readonly IMemoryCache _cache;
34
+        private readonly DataEncryptionOptions _dataEncryptionOptions;
35
+        private readonly MAX.ClientFactory _clientFactory;
36
+        private readonly MAXContext _context;
37
+
38
+        public BatchesController(ILoggerFactory loggerFactory, IMemoryCache cache,
39
+            DataEncryptionOptions dataEncryptionOptions,
40
+            MAX.ClientFactory clientFactory, MAXContext context)
41
+        {
42
+            _logger = loggerFactory.CreateLogger(GetType().FullName);
43
+            _cache = cache;
44
+            _dataEncryptionOptions = dataEncryptionOptions;
45
+            _clientFactory = clientFactory;
46
+            _context = context;
47
+        }
48
+
49
+        private IQueryable<Batch> BatchesForVendor(int vendorId)
50
+        {
51
+            return _context.Batches.Where(b => b.VendorId == vendorId);
52
+        }
53
+
54
+        [HttpGet]
55
+        public async Task<IEnumerable<Batch>> GetBatches()
56
+        {
57
+            var credentials = await Utils.GetLoginCredentialsFromRequest(HttpContext, _context);
58
+            return BatchesForVendor(credentials.Vendor.Id)
59
+                .OrderByDescending(b => b.OrderDate);
60
+        }
61
+
62
+        [HttpGet("{id}")]
63
+        public async Task<IActionResult> GetBatch([FromRoute] int id)
64
+        {
65
+            if (!ModelState.IsValid)
66
+            {
67
+                return BadRequest(ModelState);
68
+            }
69
+
70
+            var credentials = await Utils.GetLoginCredentialsFromRequest(HttpContext, _context);
71
+            var batch = await BatchesForVendor(credentials.Vendor.Id)
72
+                .Include(b => b.Account)
73
+                .SingleOrDefaultAsync(m => m.Id == id);
74
+
75
+            if (batch == null)
76
+            {
77
+                return NotFound();
78
+            }
79
+
80
+            if (!batch.ReadyForDownload)
81
+            {
82
+                await Utils.DownloadVouchersAsync(_context, batch);
83
+            }
84
+
85
+            return Ok(batch);
86
+        }
87
+
88
+        [HttpPost]
89
+        public async Task<IActionResult> PlaceOrder([FromBody] OrderRequest order)
90
+        {
91
+            if (!ModelState.IsValid)
92
+            {
93
+                return BadRequest(ModelState);
94
+            }
95
+
96
+            var credentials = await Utils.GetLoginCredentialsFromRequest(HttpContext, _context);
97
+            var catalogue = await Utils.GetProductCatalogueAsync(_clientFactory, _logger, _cache,
98
+                credentials);
99
+
100
+            Product product;
101
+            if (!catalogue.ProductMap.TryGetValue(order.ProductId.Value, out product))
102
+            {
103
+                return BadRequest("Invalid product ID");
104
+            }
105
+
106
+            var orderResponse = await MAX.Utils.PlaceOrderAsync(_clientFactory, _logger,
107
+                credentials, product, order.Quantity, order.CustomerReference,
108
+                Utils.AesDecryptBytes(credentials.Vendor.EncryptedVoucherKey,
109
+                    _dataEncryptionOptions.DefaultKey));
110
+
111
+            _context.Batches.Add(orderResponse.Batch);
112
+            credentials.User.Account.Balance = orderResponse.RemainingBalance;
113
+            await _context.SaveChangesAsync();
114
+
115
+            try
116
+            {
117
+                await Utils.DownloadVouchersAsync(_context, orderResponse.Batch);
118
+            }
119
+            catch (Exception e)
120
+            {
121
+                _logger.LogError(string.Format(
122
+                    "Failed to download vouchers for {0} batchId={1}: {2}",
123
+                    credentials.ToString(), orderResponse.Batch.Id, e.Message));
124
+            }
125
+
126
+            return Ok(orderResponse);
127
+        }
128
+    }
129
+}

+ 109 - 75
BulkPrintingAPI/Controllers/LoginController.cs

@@ -1,21 +1,19 @@
1 1
 using BulkPrintingAPI.Configuration;
2
-using BulkPrintingAPI.Db;
3
-using BulkPrintingAPI.Services;
4
-using MAX.Models;
5 2
 using Microsoft.AspNetCore.Authorization;
6 3
 using Microsoft.AspNetCore.Mvc;
7
-using Microsoft.AspNetCore.Mvc.ModelBinding;
8 4
 using Microsoft.Extensions.Logging;
5
+using Newtonsoft.Json;
9 6
 using System;
10 7
 using System.ComponentModel.DataAnnotations;
11 8
 using System.IdentityModel.Tokens.Jwt;
12 9
 using System.Security.Claims;
10
+using System.Text;
13 11
 using System.Threading.Tasks;
14 12
 
15 13
 namespace BulkPrintingAPI.Controllers
16 14
 {
17
-    //[Produces("application/json")]
18
-    [Route("api/[controller]")]
15
+    [Produces("application/json")]
16
+    [Route("api/v1/[controller]")]
19 17
     public class LoginController : Controller
20 18
     {
21 19
         public class LoginRequest
@@ -36,93 +34,129 @@ namespace BulkPrintingAPI.Controllers
36 34
             public string Password { get; set; }
37 35
         }
38 36
 
39
-        private ILogger logger;
40
-        private TokenAuthenticationOptions tokenAuthenticationOptions;
41
-        private MAXClientFactory clientFactory;
42
-        private MAXDbContext dbContext;
37
+        private readonly ILogger _logger;
38
+        private readonly TokenAuthenticationOptions _tokenAuthenticationOptions;
39
+        private readonly DataEncryptionOptions _dataEncryptionOptions;
40
+        private readonly MAX.ClientFactory _clientFactory;
41
+        private readonly MAX.Models.MAXContext _context;
43 42
 
44 43
         public LoginController(ILoggerFactory loggerFactory,
45 44
             TokenAuthenticationOptions tokenAuthenticationOptions,
46
-            MAXClientFactory clientFactory,
47
-            MAXDbContext dbContext)
45
+            DataEncryptionOptions dataEncryptionOptions,
46
+            MAX.ClientFactory clientFactory,
47
+            MAX.Models.MAXContext context)
48 48
         {
49
-            logger = loggerFactory.CreateLogger(GetType().FullName);
50
-            this.tokenAuthenticationOptions = tokenAuthenticationOptions;
51
-            this.clientFactory = clientFactory;
52
-            this.dbContext = dbContext;
49
+            _logger = loggerFactory.CreateLogger(GetType().FullName);
50
+            _tokenAuthenticationOptions = tokenAuthenticationOptions;
51
+            _dataEncryptionOptions = dataEncryptionOptions;
52
+            _clientFactory = clientFactory;
53
+            _context = context;
53 54
         }
54 55
 
55 56
         [HttpPost]
56 57
         [AllowAnonymous]
57 58
         public async Task<IActionResult> Post([FromBody] LoginRequest loginRequest)
58 59
         {
59
-            if (! ModelState.IsValid)
60
+            if (!ModelState.IsValid)
60 61
                 return BadRequest();
61 62
 
62
-            using (var client = clientFactory.GetClient(logger, loginRequest.VendorId.Value,
63
-                loginRequest.SerialNumber, loginRequest.UserId.Value, loginRequest.Username,
64
-                loginRequest.Password))
63
+            MAX.Models.User user;
64
+            try
65 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[] {
66
+                user = await MAX.Utils.AuthenticateUserAsync(_clientFactory, _logger,
67
+                    loginRequest.UserId.Value, loginRequest.Username, loginRequest.VendorId.Value,
68
+                    loginRequest.SerialNumber, loginRequest.Password);
69
+            }
70
+            catch (Exception e)
71
+            {
72
+                _logger.LogError(
73
+                    "AuthenticateUserAsync failed for {0}: {1}",
74
+                    MAX.LoginCredentials.Format(loginRequest.UserId.Value, loginRequest.Username,
75
+                        loginRequest.VendorId.Value, loginRequest.SerialNumber),
76
+                    e.Message);
77
+                return Unauthorized();
78
+            }
79
+
80
+            if (user == null)
81
+            {
82
+                _logger.LogInformation(
83
+                    "Login failed for {0}",
84
+                    MAX.LoginCredentials.Format(loginRequest.UserId.Value, loginRequest.Username,
85
+                        loginRequest.VendorId.Value, loginRequest.SerialNumber));
86
+                return Unauthorized();
87
+            }
88
+
89
+            MAX.LoginCredentials credentials;
90
+            try
91
+            {
92
+                credentials = await Utils.SyncUserAndVendorWithDb(_context, _dataEncryptionOptions,
93
+                    user, loginRequest.Password, loginRequest.VendorId.Value, loginRequest.SerialNumber);
94
+            }
95
+            catch (Exception e)
96
+            {
97
+                _logger.LogError(
98
+                    "SyncUserAndVendorWithDb failed for {0}: {1}",
99
+                    MAX.LoginCredentials.Format(loginRequest.UserId.Value, loginRequest.Username,
100
+                        loginRequest.VendorId.Value, loginRequest.SerialNumber),
101
+                    e.Message);
102
+                return Unauthorized();
103
+            }
104
+
105
+            var now = DateTime.UtcNow;
106
+            var encodedJwt = new JwtSecurityTokenHandler().CreateEncodedJwt(
107
+                issuer: _tokenAuthenticationOptions.Issuer,
108
+                audience: _tokenAuthenticationOptions.Audience,
109
+                subject: new ClaimsIdentity(new Claim[] {
109 110
                        new Claim(JwtRegisteredClaimNames.Sub, user.Id.ToString()),
110 111
                        new Claim("xpwd", loginRequest.Password),
111 112
                        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 {
113
+                       new Claim(JwtRegisteredClaimNames.Jti, await _tokenAuthenticationOptions.NonceGenerator())
114
+                }),
115
+                notBefore: now,
116
+                expires: now.Add(_tokenAuthenticationOptions.Lifetime),
117
+                issuedAt: now,
118
+                signingCredentials: _tokenAuthenticationOptions.SigningCredentials,
119
+                encryptingCredentials: _tokenAuthenticationOptions.EncryptingCredentials
120
+            );
121
+
122
+            var derivedUserKey = DataEncryptionOptions.DeriveKey(credentials.Password +
123
+                _dataEncryptionOptions.PasswordNoise, _dataEncryptionOptions.DefaultSaltLength,
124
+                _dataEncryptionOptions.DefaultIterations);
125
+            var userKey = derivedUserKey.GetBytes(32);
126
+
127
+            var payload = Convert.ToBase64String(Encoding.UTF8.GetBytes(
128
+                JsonConvert.SerializeObject(new
129
+                    {
130
+                        user = credentials.User,
131
+                        vendor = credentials.Vendor,
132
+                        encryptedDatabasePassword = Convert.ToBase64String(Utils.AesReEncrypt(
133
+                            credentials.Vendor.EncryptedDatabasePassword,
134
+                            _dataEncryptionOptions.DefaultKey,
135
+                            userKey)),
136
+                        encryptedVoucherKey = Convert.ToBase64String(Utils.AesReEncrypt(
137
+                            credentials.Vendor.EncryptedVoucherKey,
138
+                            _dataEncryptionOptions.DefaultKey,
139
+                            userKey))
140
+                    })));
141
+
142
+            byte[] signature;
143
+            using (var hmac = new System.Security.Cryptography.HMACSHA256() { Key = userKey })
144
+            {
145
+                signature = hmac.ComputeHash(Encoding.ASCII.GetBytes(payload));
146
+            }
147
+
148
+            return Ok(new
149
+                {
122 150
                     access_token = encodedJwt,
123
-                    expires_in = (int)tokenAuthenticationOptions.Lifetime.TotalSeconds
151
+                    credentials = new
152
+                    {
153
+                        payload = payload,
154
+                        salt = Convert.ToBase64String(derivedUserKey.Salt),
155
+                        iterations = derivedUserKey.IterationCount,
156
+                        signature = Convert.ToBase64String(signature)
157
+                    },
158
+                    expires_in = (int)_tokenAuthenticationOptions.Lifetime.TotalSeconds
124 159
                 });
125
-            }
126 160
         }
127 161
     }
128 162
 }

+ 37 - 0
BulkPrintingAPI/Controllers/ProductsController.cs

@@ -0,0 +1,37 @@
1
+using MAX.Models;
2
+using Microsoft.AspNetCore.Mvc;
3
+using Microsoft.Extensions.Caching.Memory;
4
+using Microsoft.Extensions.Logging;
5
+using System.Collections.Generic;
6
+using System.Threading.Tasks;
7
+
8
+namespace BulkPrintingAPI.Controllers
9
+{
10
+    [Produces("application/json")]
11
+    [Route("api/v1/[controller]")]
12
+    public class ProductsController : Controller
13
+    {
14
+        private readonly ILogger _logger;
15
+        private readonly IMemoryCache _cache;
16
+        private readonly MAX.ClientFactory _clientFactory;
17
+        private readonly MAXContext _context;
18
+
19
+        public ProductsController(ILoggerFactory loggerFactory, IMemoryCache cache,
20
+            MAX.ClientFactory clientFactory, MAXContext context)
21
+        {
22
+            _cache = cache;
23
+            _logger = loggerFactory.CreateLogger(GetType().FullName);
24
+            _clientFactory = clientFactory;
25
+            _context = context;
26
+        }
27
+
28
+        [HttpGet]
29
+        public async Task<List<Network>> Get()
30
+        {
31
+            var credentials = await Utils.GetLoginCredentialsFromRequest(HttpContext, _context);
32
+            var catalogue = await Utils.GetProductCatalogueAsync(_clientFactory, _logger, _cache,
33
+                credentials);
34
+            return catalogue.Networks;
35
+        }
36
+    }
37
+}

+ 353 - 0
BulkPrintingAPI/Controllers/Utils.cs

@@ -0,0 +1,353 @@
1
+using BulkPrintingAPI.Configuration;
2
+using CoreFtp;
3
+using MAX.Models;
4
+using Microsoft.AspNetCore.Http;
5
+using Microsoft.EntityFrameworkCore;
6
+using Microsoft.Extensions.Caching.Memory;
7
+using Microsoft.Extensions.Logging;
8
+using System;
9
+using System.Diagnostics;
10
+using System.IO;
11
+using System.Security.Claims;
12
+using System.Security.Cryptography;
13
+using System.Text;
14
+using System.Threading.Tasks;
15
+
16
+namespace BulkPrintingAPI.Controllers
17
+{
18
+    public static class Utils
19
+    {
20
+        public static byte GetMinimalBitmask(byte value)
21
+        {
22
+            if (value == 0)
23
+            {
24
+                return 0;
25
+            }
26
+            value--;
27
+            value |= (byte)(value >> 1);
28
+            value |= (byte)(value >> 2);
29
+            value |= (byte)(value >> 4);
30
+            return value;
31
+        }
32
+
33
+        public static string GenerateRandomStringFromAlphabet(int length, string alphabet =
34
+            "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+=[{]};:<>|./?")
35
+        {
36
+            if (alphabet.Length > 255)
37
+            {
38
+                throw new ArgumentException("Alphabet too long", nameof(alphabet));
39
+            }
40
+
41
+            var minimalBitmask = GetMinimalBitmask((byte)alphabet.Length);
42
+            var builder = new StringBuilder(length);
43
+            var randomBytes = new byte[length * 2];
44
+            int offset = randomBytes.Length;
45
+            using (var rng = RandomNumberGenerator.Create())
46
+            {
47
+                while (builder.Length < length)
48
+                {
49
+                    if (offset >= randomBytes.Length)
50
+                    {
51
+                        rng.GetBytes(randomBytes);
52
+                        offset = 0;
53
+                    }
54
+
55
+                    // We constrain c using minimalBitmask (minimal power of
56
+                    // two greater than alphabetSize, minus 1) so that it is as
57
+                    // close to fitting within alphabet.Length as possible while still
58
+                    // being evenly distributed. We then throw away any numbers >=
59
+                    // alphabet.Length. This is wasteful but it is the only way of
60
+                    // generating an unbiased selection, and random number
61
+                    // generators are very fast.
62
+                    byte c = (byte)(randomBytes[offset] & minimalBitmask);
63
+                    if (c < alphabet.Length)
64
+                    {
65
+                        builder.Append(alphabet[c]);
66
+                    }
67
+                    offset++;
68
+                }
69
+            }
70
+
71
+            return builder.ToString();
72
+        }
73
+
74
+        public static byte[] GenerateAndAesEncryptKey(int keyLength, byte[] encryptingKey)
75
+        {
76
+            var key = new byte[keyLength];
77
+            using (var rng = RandomNumberGenerator.Create())
78
+            {
79
+                rng.GetBytes(key);
80
+            }
81
+            using (var aes = Aes.Create())
82
+            using (var encryptor = aes.CreateEncryptor(encryptingKey, new byte[16]))
83
+            {
84
+                return MAX.Utils.Transform(encryptor, key);
85
+            }
86
+        }
87
+
88
+        public static byte[] GenerateAndAesEncryptPassword(int passwordLength, byte[] encryptingKey)
89
+        {
90
+            var password = GenerateRandomStringFromAlphabet(passwordLength);
91
+            using (var aes = Aes.Create())
92
+            using (var encryptor = aes.CreateEncryptor(encryptingKey, new byte[16]))
93
+            {
94
+                return MAX.Utils.Transform(encryptor, Encoding.ASCII.GetBytes(password));
95
+            }
96
+        }
97
+
98
+        public static byte[] AesDecryptBytes(byte[] cipherText, byte[] key)
99
+        {
100
+            using (var aes = Aes.Create())
101
+            using (var decryptor = aes.CreateDecryptor(key, new byte[16]))
102
+            {
103
+                return MAX.Utils.Transform(decryptor, cipherText);
104
+            }
105
+        }
106
+
107
+        public static string AesDecryptString(byte[] cipherText, byte[] key)
108
+        {
109
+            return Encoding.ASCII.GetString(AesDecryptBytes(cipherText, key));
110
+        }
111
+
112
+        public static byte[] AesReEncrypt(byte[] cipherText, byte[] originalKey, byte[] newKey)
113
+        {
114
+            using (var aes = Aes.Create())
115
+            {
116
+                byte[] plainText;
117
+                using (var decryptor = aes.CreateDecryptor(originalKey, new byte[16]))
118
+                {
119
+                    plainText = MAX.Utils.Transform(decryptor, cipherText);
120
+                }
121
+                using (var encryptor = aes.CreateEncryptor(newKey, new byte[16]))
122
+                {
123
+                    return MAX.Utils.Transform(encryptor, plainText);
124
+                }
125
+            }
126
+        }
127
+
128
+        public static async Task<MAX.LoginCredentials> SyncUserAndVendorWithDb(MAXContext context,
129
+            DataEncryptionOptions dataEncryptionOptions, User user, string password, int vendorId,
130
+            string serialNumber)
131
+        {
132
+            var warehouse = user.Account.Warehouse;
133
+            var existingWarehouse = await context.Warehouses.FindAsync(warehouse.Id).ConfigureAwait(false);
134
+            if (existingWarehouse == null)
135
+            {
136
+                context.Warehouses.Add(warehouse);
137
+            }
138
+            else
139
+            {
140
+                existingWarehouse.Name = warehouse.Name;
141
+                warehouse = existingWarehouse;
142
+            }
143
+
144
+            var account = user.Account;
145
+            var existingAccount = await context.Accounts.FindAsync(account.Id).ConfigureAwait(false);
146
+            if (existingAccount == null)
147
+            {
148
+                context.Accounts.Add(account);
149
+            }
150
+            else
151
+            {
152
+                existingAccount.Name = account.Name;
153
+                existingAccount.Status = account.Status;
154
+                existingAccount.Reference = account.Reference;
155
+                existingAccount.Balance = account.Balance;
156
+
157
+                account = existingAccount;
158
+            }
159
+            account.Warehouse = warehouse;
160
+
161
+            var existingUser = await context.Users.FindAsync(user.Id).ConfigureAwait(false);
162
+            if (existingUser == null)
163
+            {
164
+                context.Users.Add(user);
165
+            }
166
+            else
167
+            {
168
+                existingUser.Username = user.Username;
169
+                existingUser.FirstName = user.FirstName;
170
+                existingUser.Surname = user.Surname;
171
+                existingUser.Enabled = user.Enabled;
172
+                existingUser.Level = user.Level;
173
+                existingUser.System = user.System;
174
+                existingUser.LastLogin = user.LastLogin;
175
+
176
+                existingUser.CanPrintOffline = user.CanPrintOffline;
177
+                existingUser.CanPrintOnline = user.CanPrintOnline;
178
+                existingUser.CanReprintOffline = user.CanReprintOffline;
179
+                existingUser.CanReprintOnline = user.CanReprintOnline;
180
+                existingUser.OfflinePrintValue = user.OfflinePrintValue;
181
+                existingUser.OfflineReprintValue = user.OfflineReprintValue;
182
+                existingUser.OnlinePrintValue = user.OnlinePrintValue;
183
+                existingUser.OnlineReprintValue = user.OnlineReprintValue;
184
+
185
+                user = existingUser;
186
+            }
187
+            user.Account = account;
188
+
189
+            var vendor = await context.Vendors.FindAsync(vendorId).ConfigureAwait(false);
190
+            if (vendor == null)
191
+            {
192
+                vendor = new Vendor()
193
+                {
194
+                    Id = vendorId,
195
+                    SerialNumber = serialNumber,
196
+                    Account = account,
197
+                    EncryptedDatabasePassword =
198
+                        // A password length of 31 bytes results keeps us inside 2 AES blocks (32 bytes)
199
+                        // as padding is a minimum of 1 byte.
200
+                        GenerateAndAesEncryptPassword(31, dataEncryptionOptions.DefaultKey),
201
+                    EncryptedVoucherKey =
202
+                        GenerateAndAesEncryptKey(24, dataEncryptionOptions.DefaultKey)
203
+                };
204
+                context.Add(vendor);
205
+            }
206
+            else
207
+            {
208
+                vendor.SerialNumber = serialNumber;
209
+                vendor.Account = account;
210
+            }
211
+
212
+            await context.SaveChangesAsync().ConfigureAwait(false);
213
+
214
+            return new MAX.LoginCredentials
215
+            {
216
+                User = user,
217
+                Vendor = vendor,
218
+                Password = password
219
+            };
220
+        }
221
+
222
+        public static async Task<MAX.LoginCredentials> GetLoginCredentialsFromRequest(
223
+            HttpContext httpContext, MAXContext dbContext)
224
+        {
225
+            var authInfo = await httpContext.Authentication.GetAuthenticateInfoAsync("Bearer");
226
+            if (authInfo == null)
227
+            {
228
+                throw new Exception("Not authenticated");
229
+            }
230
+
231
+            int userId = -1;
232
+            int vendorId = -1;
233
+            string password = null;
234
+            foreach (var claim in authInfo.Principal.Claims)
235
+            {
236
+                switch (claim.Type)
237
+                {
238
+                    case ClaimTypes.NameIdentifier:
239
+                        userId = int.Parse(claim.Value);
240
+                        break;
241
+                    case "xvid":
242
+                        vendorId = int.Parse(claim.Value);
243
+                        break;
244
+                    case "xpwd":
245
+                        password = claim.Value;
246
+                        break;
247
+                }
248
+            }
249
+
250
+            var credentials = new MAX.LoginCredentials()
251
+            {
252
+                User = await dbContext.Users
253
+                    .Include(u => u.Account)
254
+                    .ThenInclude(a => a.Warehouse)
255
+                    .SingleOrDefaultAsync(u => u.Id == userId)
256
+                    .ConfigureAwait(false),
257
+                Vendor = await dbContext.Vendors
258
+                    .Include(v => v.Account) // Technically not necessary as the account is loaded above
259
+                    .SingleOrDefaultAsync(v => v.Id == vendorId)
260
+                    .ConfigureAwait(false),
261
+                Password = password
262
+            };
263
+
264
+            if ((credentials.User == null) || (credentials.Vendor == null))
265
+            {
266
+                throw new Exception(String.Format(
267
+                    "Missing user or vendor information for userId={0} vendorId={1}",
268
+                    userId, vendorId));
269
+            }
270
+
271
+            return credentials;
272
+        }
273
+
274
+        public static async Task<ProductCatalogue> GetProductCatalogueAsync(
275
+            MAX.ClientFactory clientFactory, ILogger logger, IMemoryCache cache,
276
+            MAX.LoginCredentials credentials)
277
+        {
278
+            return await cache.GetOrCreateAsync(
279
+                CacheKeys.GetAccountProductsKey(credentials.User.AccountId),
280
+                entry =>
281
+                {
282
+                    // TODO: set expiry time, etc.
283
+                    return MAX.Utils.GetProductCatalogueAsync(clientFactory, logger, credentials);
284
+                }).ConfigureAwait(false);
285
+        }
286
+
287
+        [Conditional("DEBUG")]
288
+        private static void VoucherSanityCheck(Batch batch, decimal faceValue, int batchId,
289
+            string productDescription)
290
+        {
291
+            if (batch.FaceValue != faceValue)
292
+            {
293
+                throw new Exception("Voucher face value mismatch");
294
+            }
295
+            if (batch.Id != batchId)
296
+            {
297
+                throw new Exception("Voucher batch ID mismatch");
298
+            }
299
+            if (batch.ProductDescription != productDescription)
300
+            {
301
+                throw new Exception("Voucher product description mismatch");
302
+            }
303
+        }
304
+
305
+        public static async Task DownloadVouchersAsync(MAXContext context, Batch batch)
306
+        {
307
+            var remoteFileName = string.Format("{0}_{1}.dat", batch.Account.Id, batch.Id);
308
+            using (var voucherStream = new MemoryStream())
309
+            {
310
+                using (var ftp = new FtpClient(new FtpClientConfiguration()
311
+                {
312
+                    Host = "41.223.25.5",
313
+                    Username = "anonymous",
314
+                    Password = "andrew@"
315
+                }))
316
+                {
317
+                    await ftp.LoginAsync().ConfigureAwait(false);
318
+                    using (var downloadStream = await ftp.OpenFileReadStreamAsync(remoteFileName)
319
+                        .ConfigureAwait(false))
320
+                    {
321
+                        await downloadStream.CopyToAsync(voucherStream).ConfigureAwait(false);
322
+                    }
323
+                }
324
+
325
+                voucherStream.Position = 0;
326
+                using (var streamReader = new StreamReader(voucherStream))
327
+                {
328
+                    while (streamReader.Peek() >= 0)
329
+                    {
330
+                        var line = streamReader.ReadLine();
331
+                        var parts = line.Split('|');
332
+
333
+                        VoucherSanityCheck(batch, decimal.Parse(parts[4]), int.Parse(parts[5]),
334
+                            parts[7]);
335
+
336
+                        context.Add(new Voucher()
337
+                        {
338
+                            Id = int.Parse(parts[0]),
339
+                            ExpiryDate = DateTime.Parse(parts[1]),
340
+                            Serial = parts[2],
341
+                            EncryptedPIN = parts[3],
342
+                            SequenceNumber = int.Parse(parts[6]),
343
+                            Batch = batch
344
+                        });
345
+                    }
346
+                }
347
+            }
348
+
349
+            batch.ReadyForDownload = true;
350
+            await context.SaveChangesAsync().ConfigureAwait(false);
351
+        }
352
+    }
353
+}

+ 0 - 42
BulkPrintingAPI/Controllers/ValuesController.cs

@@ -1,42 +0,0 @@
1
-using Microsoft.AspNetCore.Authorization;
2
-using Microsoft.AspNetCore.Mvc;
3
-using System.Collections.Generic;
4
-
5
-namespace BulkPrintingAPI.Controllers
6
-{
7
-    [Route("api/[controller]")]
8
-    public class ValuesController : Controller
9
-    {
10
-        // GET api/values
11
-        [HttpGet]
12
-        public IEnumerable<string> Get()
13
-        {
14
-            return new string[] { "value1", "value2" };
15
-        }
16
-
17
-        // GET api/values/5
18
-        [HttpGet("{id}")]
19
-        public string Get(int id)
20
-        {
21
-            return "value";
22
-        }
23
-
24
-        // POST api/values
25
-        [HttpPost]
26
-        public void Post([FromBody]string value)
27
-        {
28
-        }
29
-
30
-        // PUT api/values/5
31
-        [HttpPut("{id}")]
32
-        public void Put(int id, [FromBody]string value)
33
-        {
34
-        }
35
-
36
-        // DELETE api/values/5
37
-        [HttpDelete("{id}")]
38
-        public void Delete(int id)
39
-        {
40
-        }
41
-    }
42
-}

+ 68 - 0
BulkPrintingAPI/Controllers/VouchersController.cs

@@ -0,0 +1,68 @@
1
+using BulkPrintingAPI.Configuration;
2
+using MAX.Models;
3
+using Microsoft.AspNetCore.Mvc;
4
+using Microsoft.EntityFrameworkCore;
5
+using Microsoft.Extensions.Logging;
6
+using System.Linq;
7
+using System.Threading.Tasks;
8
+
9
+namespace BulkPrintingAPI.Controllers
10
+{
11
+    [Produces("application/json")]
12
+    [Route("api/v1/batches/{batchId}/[controller]")]
13
+    public class VouchersController : Controller
14
+    {
15
+        private readonly ILogger _logger;
16
+        private readonly DataEncryptionOptions _dataEncryptionOptions;
17
+        private readonly MAXContext _context;
18
+
19
+        public VouchersController(ILoggerFactory loggerFactory,
20
+            DataEncryptionOptions dataEncryptionOptions, MAXContext context)
21
+        {
22
+            _logger = loggerFactory.CreateLogger(GetType().FullName);
23
+            _dataEncryptionOptions = dataEncryptionOptions;
24
+            _context = context;
25
+        }
26
+
27
+        [HttpGet]
28
+        public async Task<IActionResult> GetVouchers([FromRoute] int batchId)
29
+        {
30
+            if (!ModelState.IsValid)
31
+            {
32
+                return BadRequest(ModelState);
33
+            }
34
+
35
+            var credentials = await Utils.GetLoginCredentialsFromRequest(HttpContext, _context);
36
+            return Ok(VouchersForBatchAndVendor(batchId, credentials.Vendor.Id)
37
+                .OrderBy(v => new { v.BatchId, v.SequenceNumber }));
38
+        }
39
+
40
+        [HttpGet("{sequenceNumber}")]
41
+        public async Task<IActionResult> GetVoucher([FromRoute] int batchId, [FromRoute] int sequenceNumber)
42
+        {
43
+            if (!ModelState.IsValid)
44
+            {
45
+                return BadRequest(ModelState);
46
+            }
47
+
48
+            var credentials = await Utils.GetLoginCredentialsFromRequest(HttpContext, _context);
49
+            var voucher = await VouchersForBatchAndVendor(batchId, credentials.Vendor.Id)
50
+                .SingleOrDefaultAsync(v => (v.BatchId == batchId) && (v.SequenceNumber == sequenceNumber));
51
+
52
+            if (voucher == null)
53
+            {
54
+                return NotFound();
55
+            }
56
+
57
+            return Ok(voucher);
58
+        }
59
+
60
+
61
+        private IQueryable<Voucher> VouchersForBatchAndVendor(int batchId, int vendorId)
62
+        {
63
+            return _context.Vouchers
64
+                .Include(v => v.Batch)
65
+                .Where(v => (v.BatchId == batchId) && (v.Batch.VendorId == vendorId));
66
+        }
67
+    }
68
+}

+ 0 - 84
BulkPrintingAPI/Db/DbHelpers.cs

@@ -1,84 +0,0 @@
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
-}

+ 111 - 2
BulkPrintingAPI/Migrations/20170701135738_InitialCreate.Designer.cs

@@ -7,8 +7,8 @@ using MAX.Models;
7 7
 
8 8
 namespace BulkPrintingAPI.Migrations
9 9
 {
10
-    [DbContext(typeof(MAXDbContext))]
11
-    [Migration("20170701135738_InitialCreate")]
10
+    [DbContext(typeof(MAXContext))]
11
+    [Migration("20170705113945_InitialCreate")]
12 12
     partial class InitialCreate
13 13
     {
14 14
         protected override void BuildTargetModel(ModelBuilder modelBuilder)
@@ -24,9 +24,11 @@ namespace BulkPrintingAPI.Migrations
24 24
                     b.Property<decimal>("Balance");
25 25
 
26 26
                     b.Property<string>("Name")
27
+                        .IsRequired()
27 28
                         .HasMaxLength(50);
28 29
 
29 30
                     b.Property<string>("Reference")
31
+                        .IsRequired()
30 32
                         .HasMaxLength(50);
31 33
 
32 34
                     b.Property<int>("Status");
@@ -40,6 +42,57 @@ namespace BulkPrintingAPI.Migrations
40 42
                     b.ToTable("Accounts");
41 43
                 });
42 44
 
45
+            modelBuilder.Entity("MAX.Models.Batch", b =>
46
+                {
47
+                    b.Property<int>("Id");
48
+
49
+                    b.Property<int>("AccountId");
50
+
51
+                    b.Property<decimal>("Cost");
52
+
53
+                    b.Property<int>("DeliveredQuantity");
54
+
55
+                    b.Property<decimal>("DiscountPercentage");
56
+
57
+                    b.Property<decimal>("FaceValue");
58
+
59
+                    b.Property<int>("NetworkId");
60
+
61
+                    b.Property<string>("NetworkName")
62
+                        .IsRequired()
63
+                        .HasMaxLength(20);
64
+
65
+                    b.Property<DateTime>("OrderDate");
66
+
67
+                    b.Property<string>("OrderReference")
68
+                        .IsRequired()
69
+                        .HasMaxLength(20);
70
+
71
+                    b.Property<int>("OrderedById");
72
+
73
+                    b.Property<string>("ProductDescription")
74
+                        .IsRequired()
75
+                        .HasMaxLength(50);
76
+
77
+                    b.Property<int>("ProductId");
78
+
79
+                    b.Property<bool>("ReadyForDownload");
80
+
81
+                    b.Property<int>("RequestedQuantity");
82
+
83
+                    b.Property<int>("VendorId");
84
+
85
+                    b.Property<int>("VoucherType");
86
+
87
+                    b.HasKey("Id");
88
+
89
+                    b.HasIndex("AccountId");
90
+
91
+                    b.HasIndex("ReadyForDownload", "OrderDate");
92
+
93
+                    b.ToTable("Batches");
94
+                });
95
+
43 96
             modelBuilder.Entity("MAX.Models.User", b =>
44 97
                 {
45 98
                     b.Property<int>("Id");
@@ -57,6 +110,7 @@ namespace BulkPrintingAPI.Migrations
57 110
                     b.Property<bool>("Enabled");
58 111
 
59 112
                     b.Property<string>("FirstName")
113
+                        .IsRequired()
60 114
                         .HasMaxLength(50);
61 115
 
62 116
                     b.Property<DateTime>("LastLogin");
@@ -72,11 +126,13 @@ namespace BulkPrintingAPI.Migrations
72 126
                     b.Property<decimal>("OnlineReprintValue");
73 127
 
74 128
                     b.Property<string>("Surname")
129
+                        .IsRequired()
75 130
                         .HasMaxLength(50);
76 131
 
77 132
                     b.Property<int>("System");
78 133
 
79 134
                     b.Property<string>("Username")
135
+                        .IsRequired()
80 136
                         .HasMaxLength(50);
81 137
 
82 138
                     b.HasKey("Id");
@@ -92,7 +148,16 @@ namespace BulkPrintingAPI.Migrations
92 148
 
93 149
                     b.Property<int>("AccountId");
94 150
 
151
+                    b.Property<byte[]>("EncryptedDatabasePassword")
152
+                        .IsRequired()
153
+                        .HasMaxLength(32);
154
+
155
+                    b.Property<byte[]>("EncryptedVoucherKey")
156
+                        .IsRequired()
157
+                        .HasMaxLength(32);
158
+
95 159
                     b.Property<string>("SerialNumber")
160
+                        .IsRequired()
96 161
                         .HasMaxLength(50);
97 162
 
98 163
                     b.HasKey("Id");
@@ -102,11 +167,39 @@ namespace BulkPrintingAPI.Migrations
102 167
                     b.ToTable("Vendors");
103 168
                 });
104 169
 
170
+            modelBuilder.Entity("MAX.Models.Voucher", b =>
171
+                {
172
+                    b.Property<int>("Id");
173
+
174
+                    b.Property<int>("BatchId");
175
+
176
+                    b.Property<string>("EncryptedPIN")
177
+                        .IsRequired()
178
+                        .HasMaxLength(50);
179
+
180
+                    b.Property<DateTime>("ExpiryDate")
181
+                        .HasColumnType("Date");
182
+
183
+                    b.Property<int>("SequenceNumber");
184
+
185
+                    b.Property<string>("Serial")
186
+                        .IsRequired()
187
+                        .HasMaxLength(50);
188
+
189
+                    b.HasKey("Id");
190
+
191
+                    b.HasIndex("BatchId", "SequenceNumber")
192
+                        .IsUnique();
193
+
194
+                    b.ToTable("Vouchers");
195
+                });
196
+
105 197
             modelBuilder.Entity("MAX.Models.Warehouse", b =>
106 198
                 {
107 199
                     b.Property<int>("Id");
108 200
 
109 201
                     b.Property<string>("Name")
202
+                        .IsRequired()
110 203
                         .HasMaxLength(50);
111 204
 
112 205
                     b.HasKey("Id");
@@ -122,6 +215,14 @@ namespace BulkPrintingAPI.Migrations
122 215
                         .OnDelete(DeleteBehavior.Cascade);
123 216
                 });
124 217
 
218
+            modelBuilder.Entity("MAX.Models.Batch", b =>
219
+                {
220
+                    b.HasOne("MAX.Models.Account", "Account")
221
+                        .WithMany()
222
+                        .HasForeignKey("AccountId")
223
+                        .OnDelete(DeleteBehavior.Cascade);
224
+                });
225
+
125 226
             modelBuilder.Entity("MAX.Models.User", b =>
126 227
                 {
127 228
                     b.HasOne("MAX.Models.Account", "Account")
@@ -137,6 +238,14 @@ namespace BulkPrintingAPI.Migrations
137 238
                         .HasForeignKey("AccountId")
138 239
                         .OnDelete(DeleteBehavior.Cascade);
139 240
                 });
241
+
242
+            modelBuilder.Entity("MAX.Models.Voucher", b =>
243
+                {
244
+                    b.HasOne("MAX.Models.Batch", "Batch")
245
+                        .WithMany("Vouchers")
246
+                        .HasForeignKey("BatchId")
247
+                        .OnDelete(DeleteBehavior.Cascade);
248
+                });
140 249
         }
141 250
     }
142 251
 }

+ 86 - 7
BulkPrintingAPI/Migrations/20170701135738_InitialCreate.cs

@@ -13,7 +13,7 @@ namespace BulkPrintingAPI.Migrations
13 13
                 columns: table => new
14 14
                 {
15 15
                     Id = table.Column<int>(nullable: false),
16
-                    Name = table.Column<string>(maxLength: 50, nullable: true)
16
+                    Name = table.Column<string>(maxLength: 50, nullable: false)
17 17
                 },
18 18
                 constraints: table =>
19 19
                 {
@@ -26,8 +26,8 @@ namespace BulkPrintingAPI.Migrations
26 26
                 {
27 27
                     Id = table.Column<int>(nullable: false),
28 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),
29
+                    Name = table.Column<string>(maxLength: 50, nullable: false),
30
+                    Reference = table.Column<string>(maxLength: 50, nullable: false),
31 31
                     Status = table.Column<int>(nullable: false),
32 32
                     WarehouseId = table.Column<int>(nullable: false)
33 33
                 },
@@ -43,6 +43,39 @@ namespace BulkPrintingAPI.Migrations
43 43
                 });
44 44
 
45 45
             migrationBuilder.CreateTable(
46
+                name: "Batches",
47
+                columns: table => new
48
+                {
49
+                    Id = table.Column<int>(nullable: false),
50
+                    AccountId = table.Column<int>(nullable: false),
51
+                    Cost = table.Column<decimal>(nullable: false),
52
+                    DeliveredQuantity = table.Column<int>(nullable: false),
53
+                    DiscountPercentage = table.Column<decimal>(nullable: false),
54
+                    FaceValue = table.Column<decimal>(nullable: false),
55
+                    NetworkId = table.Column<int>(nullable: false),
56
+                    NetworkName = table.Column<string>(maxLength: 20, nullable: false),
57
+                    OrderDate = table.Column<DateTime>(nullable: false),
58
+                    OrderReference = table.Column<string>(maxLength: 20, nullable: false),
59
+                    OrderedById = table.Column<int>(nullable: false),
60
+                    ProductDescription = table.Column<string>(maxLength: 50, nullable: false),
61
+                    ProductId = table.Column<int>(nullable: false),
62
+                    ReadyForDownload = table.Column<bool>(nullable: false),
63
+                    RequestedQuantity = table.Column<int>(nullable: false),
64
+                    VendorId = table.Column<int>(nullable: false),
65
+                    VoucherType = table.Column<int>(nullable: false)
66
+                },
67
+                constraints: table =>
68
+                {
69
+                    table.PrimaryKey("PK_Batches", x => x.Id);
70
+                    table.ForeignKey(
71
+                        name: "FK_Batches_Accounts_AccountId",
72
+                        column: x => x.AccountId,
73
+                        principalTable: "Accounts",
74
+                        principalColumn: "Id",
75
+                        onDelete: ReferentialAction.Cascade);
76
+                });
77
+
78
+            migrationBuilder.CreateTable(
46 79
                 name: "Users",
47 80
                 columns: table => new
48 81
                 {
@@ -53,16 +86,16 @@ namespace BulkPrintingAPI.Migrations
53 86
                     CanReprintOffline = table.Column<bool>(nullable: false),
54 87
                     CanReprintOnline = table.Column<bool>(nullable: false),
55 88
                     Enabled = table.Column<bool>(nullable: false),
56
-                    FirstName = table.Column<string>(maxLength: 50, nullable: true),
89
+                    FirstName = table.Column<string>(maxLength: 50, nullable: false),
57 90
                     LastLogin = table.Column<DateTime>(nullable: false),
58 91
                     Level = table.Column<int>(nullable: false),
59 92
                     OfflinePrintValue = table.Column<decimal>(nullable: false),
60 93
                     OfflineReprintValue = table.Column<decimal>(nullable: false),
61 94
                     OnlinePrintValue = table.Column<decimal>(nullable: false),
62 95
                     OnlineReprintValue = table.Column<decimal>(nullable: false),
63
-                    Surname = table.Column<string>(maxLength: 50, nullable: true),
96
+                    Surname = table.Column<string>(maxLength: 50, nullable: false),
64 97
                     System = table.Column<int>(nullable: false),
65
-                    Username = table.Column<string>(maxLength: 50, nullable: true)
98
+                    Username = table.Column<string>(maxLength: 50, nullable: false)
66 99
                 },
67 100
                 constraints: table =>
68 101
                 {
@@ -81,7 +114,9 @@ namespace BulkPrintingAPI.Migrations
81 114
                 {
82 115
                     Id = table.Column<int>(nullable: false),
83 116
                     AccountId = table.Column<int>(nullable: false),
84
-                    SerialNumber = table.Column<string>(maxLength: 50, nullable: true)
117
+                    EncryptedDatabasePassword = table.Column<byte[]>(maxLength: 32, nullable: false),
118
+                    EncryptedVoucherKey = table.Column<byte[]>(maxLength: 32, nullable: false),
119
+                    SerialNumber = table.Column<string>(maxLength: 50, nullable: false)
85 120
                 },
86 121
                 constraints: table =>
87 122
                 {
@@ -94,12 +129,44 @@ namespace BulkPrintingAPI.Migrations
94 129
                         onDelete: ReferentialAction.Cascade);
95 130
                 });
96 131
 
132
+            migrationBuilder.CreateTable(
133
+                name: "Vouchers",
134
+                columns: table => new
135
+                {
136
+                    Id = table.Column<int>(nullable: false),
137
+                    BatchId = table.Column<int>(nullable: false),
138
+                    EncryptedPIN = table.Column<string>(maxLength: 50, nullable: false),
139
+                    ExpiryDate = table.Column<DateTime>(type: "Date", nullable: false),
140
+                    SequenceNumber = table.Column<int>(nullable: false),
141
+                    Serial = table.Column<string>(maxLength: 50, nullable: false)
142
+                },
143
+                constraints: table =>
144
+                {
145
+                    table.PrimaryKey("PK_Vouchers", x => x.Id);
146
+                    table.ForeignKey(
147
+                        name: "FK_Vouchers_Batches_BatchId",
148
+                        column: x => x.BatchId,
149
+                        principalTable: "Batches",
150
+                        principalColumn: "Id",
151
+                        onDelete: ReferentialAction.Cascade);
152
+                });
153
+
97 154
             migrationBuilder.CreateIndex(
98 155
                 name: "IX_Accounts_WarehouseId",
99 156
                 table: "Accounts",
100 157
                 column: "WarehouseId");
101 158
 
102 159
             migrationBuilder.CreateIndex(
160
+                name: "IX_Batches_AccountId",
161
+                table: "Batches",
162
+                column: "AccountId");
163
+
164
+            migrationBuilder.CreateIndex(
165
+                name: "IX_Batches_ReadyForDownload_OrderDate",
166
+                table: "Batches",
167
+                columns: new[] { "ReadyForDownload", "OrderDate" });
168
+
169
+            migrationBuilder.CreateIndex(
103 170
                 name: "IX_Users_AccountId",
104 171
                 table: "Users",
105 172
                 column: "AccountId");
@@ -108,6 +175,12 @@ namespace BulkPrintingAPI.Migrations
108 175
                 name: "IX_Vendors_AccountId",
109 176
                 table: "Vendors",
110 177
                 column: "AccountId");
178
+
179
+            migrationBuilder.CreateIndex(
180
+                name: "IX_Vouchers_BatchId_SequenceNumber",
181
+                table: "Vouchers",
182
+                columns: new[] { "BatchId", "SequenceNumber" },
183
+                unique: true);
111 184
         }
112 185
 
113 186
         protected override void Down(MigrationBuilder migrationBuilder)
@@ -119,6 +192,12 @@ namespace BulkPrintingAPI.Migrations
119 192
                 name: "Vendors");
120 193
 
121 194
             migrationBuilder.DropTable(
195
+                name: "Vouchers");
196
+
197
+            migrationBuilder.DropTable(
198
+                name: "Batches");
199
+
200
+            migrationBuilder.DropTable(
122 201
                 name: "Accounts");
123 202
 
124 203
             migrationBuilder.DropTable(

+ 111 - 2
BulkPrintingAPI/Migrations/MAXDbContextModelSnapshot.cs

@@ -7,8 +7,8 @@ using MAX.Models;
7 7
 
8 8
 namespace BulkPrintingAPI.Migrations
9 9
 {
10
-    [DbContext(typeof(MAXDbContext))]
11
-    partial class MAXDbContextModelSnapshot : ModelSnapshot
10
+    [DbContext(typeof(MAXContext))]
11
+    partial class MAXContextModelSnapshot : ModelSnapshot
12 12
     {
13 13
         protected override void BuildModel(ModelBuilder modelBuilder)
14 14
         {
@@ -23,9 +23,11 @@ namespace BulkPrintingAPI.Migrations
23 23
                     b.Property<decimal>("Balance");
24 24
 
25 25
                     b.Property<string>("Name")
26
+                        .IsRequired()
26 27
                         .HasMaxLength(50);
27 28
 
28 29
                     b.Property<string>("Reference")
30
+                        .IsRequired()
29 31
                         .HasMaxLength(50);
30 32
 
31 33
                     b.Property<int>("Status");
@@ -39,6 +41,57 @@ namespace BulkPrintingAPI.Migrations
39 41
                     b.ToTable("Accounts");
40 42
                 });
41 43
 
44
+            modelBuilder.Entity("MAX.Models.Batch", b =>
45
+                {
46
+                    b.Property<int>("Id");
47
+
48
+                    b.Property<int>("AccountId");
49
+
50
+                    b.Property<decimal>("Cost");
51
+
52
+                    b.Property<int>("DeliveredQuantity");
53
+
54
+                    b.Property<decimal>("DiscountPercentage");
55
+
56
+                    b.Property<decimal>("FaceValue");
57
+
58
+                    b.Property<int>("NetworkId");
59
+
60
+                    b.Property<string>("NetworkName")
61
+                        .IsRequired()
62
+                        .HasMaxLength(20);
63
+
64
+                    b.Property<DateTime>("OrderDate");
65
+
66
+                    b.Property<string>("OrderReference")
67
+                        .IsRequired()
68
+                        .HasMaxLength(20);
69
+
70
+                    b.Property<int>("OrderedById");
71
+
72
+                    b.Property<string>("ProductDescription")
73
+                        .IsRequired()
74
+                        .HasMaxLength(50);
75
+
76
+                    b.Property<int>("ProductId");
77
+
78
+                    b.Property<bool>("ReadyForDownload");
79
+
80
+                    b.Property<int>("RequestedQuantity");
81
+
82
+                    b.Property<int>("VendorId");
83
+
84
+                    b.Property<int>("VoucherType");
85
+
86
+                    b.HasKey("Id");
87
+
88
+                    b.HasIndex("AccountId");
89
+
90
+                    b.HasIndex("ReadyForDownload", "OrderDate");
91
+
92
+                    b.ToTable("Batches");
93
+                });
94
+
42 95
             modelBuilder.Entity("MAX.Models.User", b =>
43 96
                 {
44 97
                     b.Property<int>("Id");
@@ -56,6 +109,7 @@ namespace BulkPrintingAPI.Migrations
56 109
                     b.Property<bool>("Enabled");
57 110
 
58 111
                     b.Property<string>("FirstName")
112
+                        .IsRequired()
59 113
                         .HasMaxLength(50);
60 114
 
61 115
                     b.Property<DateTime>("LastLogin");
@@ -71,11 +125,13 @@ namespace BulkPrintingAPI.Migrations
71 125
                     b.Property<decimal>("OnlineReprintValue");
72 126
 
73 127
                     b.Property<string>("Surname")
128
+                        .IsRequired()
74 129
                         .HasMaxLength(50);
75 130
 
76 131
                     b.Property<int>("System");
77 132
 
78 133
                     b.Property<string>("Username")
134
+                        .IsRequired()
79 135
                         .HasMaxLength(50);
80 136
 
81 137
                     b.HasKey("Id");
@@ -91,7 +147,16 @@ namespace BulkPrintingAPI.Migrations
91 147
 
92 148
                     b.Property<int>("AccountId");
93 149
 
150
+                    b.Property<byte[]>("EncryptedDatabasePassword")
151
+                        .IsRequired()
152
+                        .HasMaxLength(32);
153
+
154
+                    b.Property<byte[]>("EncryptedVoucherKey")
155
+                        .IsRequired()
156
+                        .HasMaxLength(32);
157
+
94 158
                     b.Property<string>("SerialNumber")
159
+                        .IsRequired()
95 160
                         .HasMaxLength(50);
96 161
 
97 162
                     b.HasKey("Id");
@@ -101,11 +166,39 @@ namespace BulkPrintingAPI.Migrations
101 166
                     b.ToTable("Vendors");
102 167
                 });
103 168
 
169
+            modelBuilder.Entity("MAX.Models.Voucher", b =>
170
+                {
171
+                    b.Property<int>("Id");
172
+
173
+                    b.Property<int>("BatchId");
174
+
175
+                    b.Property<string>("EncryptedPIN")
176
+                        .IsRequired()
177
+                        .HasMaxLength(50);
178
+
179
+                    b.Property<DateTime>("ExpiryDate")
180
+                        .HasColumnType("Date");
181
+
182
+                    b.Property<int>("SequenceNumber");
183
+
184
+                    b.Property<string>("Serial")
185
+                        .IsRequired()
186
+                        .HasMaxLength(50);
187
+
188
+                    b.HasKey("Id");
189
+
190
+                    b.HasIndex("BatchId", "SequenceNumber")
191
+                        .IsUnique();
192
+
193
+                    b.ToTable("Vouchers");
194
+                });
195
+
104 196
             modelBuilder.Entity("MAX.Models.Warehouse", b =>
105 197
                 {
106 198
                     b.Property<int>("Id");
107 199
 
108 200
                     b.Property<string>("Name")
201
+                        .IsRequired()
109 202
                         .HasMaxLength(50);
110 203
 
111 204
                     b.HasKey("Id");
@@ -121,6 +214,14 @@ namespace BulkPrintingAPI.Migrations
121 214
                         .OnDelete(DeleteBehavior.Cascade);
122 215
                 });
123 216
 
217
+            modelBuilder.Entity("MAX.Models.Batch", b =>
218
+                {
219
+                    b.HasOne("MAX.Models.Account", "Account")
220
+                        .WithMany()
221
+                        .HasForeignKey("AccountId")
222
+                        .OnDelete(DeleteBehavior.Cascade);
223
+                });
224
+
124 225
             modelBuilder.Entity("MAX.Models.User", b =>
125 226
                 {
126 227
                     b.HasOne("MAX.Models.Account", "Account")
@@ -136,6 +237,14 @@ namespace BulkPrintingAPI.Migrations
136 237
                         .HasForeignKey("AccountId")
137 238
                         .OnDelete(DeleteBehavior.Cascade);
138 239
                 });
240
+
241
+            modelBuilder.Entity("MAX.Models.Voucher", b =>
242
+                {
243
+                    b.HasOne("MAX.Models.Batch", "Batch")
244
+                        .WithMany("Vouchers")
245
+                        .HasForeignKey("BatchId")
246
+                        .OnDelete(DeleteBehavior.Cascade);
247
+                });
139 248
         }
140 249
     }
141 250
 }

+ 3 - 3
BulkPrintingAPI/Properties/launchSettings.json

@@ -1,4 +1,4 @@
1
-{
1
+{
2 2
   "iisSettings": {
3 3
     "windowsAuthentication": false,
4 4
     "anonymousAuthentication": true,
@@ -11,7 +11,7 @@
11 11
     "IIS Express": {
12 12
       "commandName": "IISExpress",
13 13
       "launchBrowser": true,
14
-      "launchUrl": "api/values",
14
+      "launchUrl": "api/products",
15 15
       "environmentVariables": {
16 16
         "ASPNETCORE_ENVIRONMENT": "Development"
17 17
       }
@@ -26,4 +26,4 @@
26 26
       "applicationUrl": "http://localhost:50070"
27 27
     }
28 28
   }
29
-}
29
+}

+ 5 - 5
BulkPrintingAPI/Startup.cs

@@ -28,10 +28,9 @@ namespace BulkPrintingAPI
28 28
 
29 29
         public IConfigurationRoot Configuration { get; }
30 30
 
31
-        // This method gets called by the runtime. Use this method to add services to the container.
32 31
         public void ConfigureServices(IServiceCollection services)
33 32
         {
34
-            // Add framework services.
33
+            services.AddMemoryCache();
35 34
             services.AddMvc(config =>
36 35
             {
37 36
                 var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
@@ -39,15 +38,16 @@ namespace BulkPrintingAPI
39 38
             });
40 39
             services.AddSingleton(new TokenAuthenticationOptions(
41 40
                 Configuration.GetSection("TokenAuthentication")));
42
-            services.AddSingleton(new Services.MAXClientFactory(
41
+            services.AddSingleton(new DataEncryptionOptions(
42
+                Configuration.GetSection("DataEncryption")));
43
+            services.AddSingleton(new MAX.ClientFactory(
43 44
                 Configuration.GetSection("MAX")));
44
-            services.AddDbContext<MAX.Models.MAXDbContext>(
45
+            services.AddDbContext<MAX.Models.MAXContext>(
45 46
                 options => options.UseSqlServer(
46 47
                     Configuration["Database:ConnectionString"],
47 48
                     b => b.MigrationsAssembly("BulkPrintingAPI")));
48 49
         }
49 50
 
50
-        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
51 51
         public void Configure(IApplicationBuilder app, IHostingEnvironment env,
52 52
             ILoggerFactory loggerFactory, TokenAuthenticationOptions tokenAuthenticationOptions)
53 53
         {

+ 204 - 101
MAXClient/Client.cs

@@ -2,7 +2,6 @@
2 2
 using MAX.Models;
3 3
 using Microsoft.Extensions.Logging;
4 4
 using System;
5
-using System.IO;
6 5
 using System.Net.Sockets;
7 6
 using System.Security.Cryptography;
8 7
 using System.Text;
@@ -14,37 +13,43 @@ namespace MAX
14 13
 {
15 14
     public class Client : IDisposable
16 15
     {
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;
16
+        private ILogger _logger;
17
+        private string _host;
18
+        private int _port;
19
+        private int _vendorId;
20
+        private string _serialNumber;
21
+        private int _userId;
22
+        private string _username;
23
+        private string _password;
25 24
 
26
-        private TcpClient connection = null;
27
-        private NetworkStream connectionStream = null;
28
-        private TripleDES des = null;
25
+        private TcpClient _connection = null;
26
+        private NetworkStream _connectionStream = null;
27
+        private TripleDES _des = null;
29 28
 
30
-        private bool disposed = false;
29
+        private bool _disposed = false;
31 30
 
32 31
         public Client(ILogger logger, string host, int port, int vendorId, string serialNumber, int userId, string username, string password)
33 32
         {
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;
33
+            _logger = logger;
34
+            _host = host;
35
+            _port = port;
36
+            _vendorId = vendorId;
37
+            _serialNumber = serialNumber;
38
+            _userId = userId;
39
+            _username = username;
40
+            _password = password;
42 41
 
43 42
             ConnectTimeout = 10000;
44 43
             ReceiveTimeout = 10000;
45 44
             SendTimeout = 10000;
46 45
         }
47 46
 
47
+        public Client(ILogger logger, string host, int port, LoginCredentials credentials)
48
+            : this(logger, host, port, credentials.Vendor.Id, credentials.Vendor.SerialNumber,
49
+                  credentials.User.Id, credentials.User.Username, credentials.Password)
50
+        {
51
+        }
52
+
48 53
         public void Close()
49 54
         {
50 55
             Dispose(true);
@@ -52,39 +57,39 @@ namespace MAX
52 57
 
53 58
         public async Task<User> ConnectAsync()
54 59
         {
55
-            if (connection != null)
60
+            if (_connection != null)
56 61
                 throw new Exception("Already connected");
57 62
 
58
-            connection = new TcpClient(AddressFamily.InterNetwork);
59
-            connection.ReceiveTimeout = ReceiveTimeout;
60
-            connection.SendTimeout = SendTimeout;
63
+            _connection = new TcpClient(AddressFamily.InterNetwork);
64
+            _connection.ReceiveTimeout = ReceiveTimeout;
65
+            _connection.SendTimeout = SendTimeout;
61 66
 
62 67
             // Connect to the server
63 68
             try
64 69
             {
65 70
                 using (var cancellationSource = new CancellationTokenSource(ConnectTimeout))
66 71
                 {
67
-                    await connection.ConnectAsync(host, port).WithCancellation(cancellationSource.Token).ConfigureAwait(false);
72
+                    await _connection.ConnectAsync(_host, _port).WithCancellation(cancellationSource.Token).ConfigureAwait(false);
68 73
                 }
69 74
             }
70 75
             catch (OperationCanceledException)
71 76
             {
72 77
                 throw new Exception("Connect timeout");
73 78
             }
74
-            connectionStream = connection.GetStream();
79
+            _connectionStream = _connection.GetStream();
75 80
 
76 81
             // Device authentication
77 82
             await WriteMessageAsync(new MessageBuilder()
78 83
                 .Append("Hi ")
79
-                .Append(serialNumber)
84
+                .Append(_serialNumber)
80 85
                 .Append("|V")
81
-                .Append(vendorId)
86
+                .Append(_vendorId)
82 87
                 .Append("|123451234512345||||||")).ConfigureAwait(false);
83 88
 
84 89
             var response = await ReadMessageAsync().ConfigureAwait(false);
85 90
             if (!response.StartsWith("Hi "))
86 91
             {
87
-                logger.LogError("Device authentication failed: {0}", response);
92
+                _logger.LogError("Device authentication failed: {0}", response);
88 93
                 return null;
89 94
             }
90 95
 
@@ -93,11 +98,11 @@ namespace MAX
93 98
             response = await ReadMessageAsync().ConfigureAwait(false);
94 99
 
95 100
             // Key exchange
96
-            des = TripleDES.Create();
97
-            des.IV = new byte[] { 0, 0, 0, 0, 0, 0, 0, 0 };
101
+            _des = TripleDES.Create();
102
+            _des.IV = new byte[8];
98 103
             await WriteMessageAsync(new MessageBuilder()
99 104
                 .Append("3D ")
100
-                .Append(EncryptRSA(response, BitConverter.ToString(des.Key).Replace("-", "")))).ConfigureAwait(false);
105
+                .Append(EncryptRSA(response, BitConverter.ToString(_des.Key).Replace("-", "")))).ConfigureAwait(false);
101 106
 
102 107
             response = await ReadMessageAsync().ConfigureAwait(false);
103 108
             if (!response.StartsWith("OK"))
@@ -109,11 +114,11 @@ namespace MAX
109 114
             await WriteMessageAsync(new MessageBuilder()
110 115
                 .Append("User ")
111 116
                 .Append(Encrypt(new StringBuilder()
112
-                    .Append(userId)
117
+                    .Append(_userId)
113 118
                     .Append("|")
114
-                    .Append(username)
119
+                    .Append(_username)
115 120
                     .Append("|")
116
-                    .Append(password).ToString()))).ConfigureAwait(false);
121
+                    .Append(_password).ToString()))).ConfigureAwait(false);
117 122
 
118 123
             response = Decrypt(await ReadMessageAsync().ConfigureAwait(false));
119 124
             if (response.StartsWith("OK"))
@@ -121,8 +126,8 @@ namespace MAX
121 126
                 var parts = response.Split('|');
122 127
                 var user = new User()
123 128
                 {
124
-                    Id = userId,
125
-                    Username = username,
129
+                    Id = _userId,
130
+                    Username = _username,
126 131
                     FirstName = parts[4],
127 132
                     Surname = parts[3],
128 133
                     Enabled = bool.Parse(parts[6]),
@@ -143,40 +148,11 @@ namespace MAX
143 148
                     user.OnlineReprintValue = decimal.Parse(parts[14]);
144 149
                 }
145 150
 
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
-                }
151
+                return user;
176 152
             }
177 153
             else if (response.StartsWith("ER"))
178 154
             {
179
-                logger.LogInformation("User authentication failed: {0}", response);
155
+                _logger.LogInformation("User authentication failed: {0}", response);
180 156
                 return null;
181 157
             }
182 158
             else
@@ -189,28 +165,28 @@ namespace MAX
189 165
 
190 166
         protected virtual void Dispose(bool disposing)
191 167
         {
192
-            if (disposed)
168
+            if (_disposed)
193 169
                 return;
194 170
 
195
-            disposed = true;
171
+            _disposed = true;
196 172
 
197 173
             // No unmanaged resources are disposed so we don't need the full finalisation pattern.
198 174
             if (disposing)
199 175
             {
200
-                if (des != null)
176
+                if (_des != null)
201 177
                 {
202
-                    des.Dispose();
203
-                    des = null;
178
+                    _des.Dispose();
179
+                    _des = null;
204 180
                 }
205
-                if (connectionStream != null)
181
+                if (_connectionStream != null)
206 182
                 {
207
-                    connectionStream.Dispose();
208
-                    connectionStream = null;
183
+                    _connectionStream.Dispose();
184
+                    _connectionStream = null;
209 185
                 }
210
-                if (connection != null)
186
+                if (_connection != null)
211 187
                 {
212
-                    connection.Dispose();
213
-                    connection = null;
188
+                    _connection.Dispose();
189
+                    _connection = null;
214 190
                 }
215 191
             }
216 192
         }
@@ -222,18 +198,12 @@ namespace MAX
222 198
 
223 199
         private string Decrypt(string cipherText)
224 200
         {
225
-            using (var decryptor = des.CreateDecryptor(des.Key, des.IV))
226
-            {
227
-                return Encoding.UTF8.GetString(Transform(decryptor, Convert.FromBase64String(cipherText)));
228
-            }
201
+            return Utils.TripleDESDecrypt(cipherText, _des);
229 202
         }
230 203
 
231 204
         private string Encrypt(string plainText)
232 205
         {
233
-            using (var encryptor = des.CreateEncryptor(des.Key, des.IV))
234
-            {
235
-                return Convert.ToBase64String(Transform(encryptor, Encoding.UTF8.GetBytes(plainText)));
236
-            }
206
+            return Utils.TripleDESEncrypt(plainText, _des);
237 207
         }
238 208
 
239 209
         private string EncryptRSA(string publicKey, string plainText)
@@ -283,13 +253,153 @@ namespace MAX
283 253
             }
284 254
         }
285 255
 
256
+        public async Task<Account> GetAccountAsync()
257
+        {
258
+            await WriteMessageAsync(new MessageBuilder().Append("Acc")).ConfigureAwait(false);
259
+            var response = Decrypt(await ReadMessageAsync().ConfigureAwait(false));
260
+            if (response.StartsWith("OK"))
261
+            {
262
+                var parts = response.Split('|');
263
+                return new Account()
264
+                {
265
+                    Id = int.Parse(parts[1]),
266
+                    Name = parts[2],
267
+                    Balance = decimal.Parse(parts[3]),
268
+                    Status = (Account.AccountStatus)int.Parse(parts[4]),
269
+                    Reference = parts[5],
270
+                    Warehouse = new Warehouse()
271
+                    {
272
+                        Id = int.Parse(parts[6]),
273
+                        Name = parts[7]
274
+                    }
275
+                };
276
+            }
277
+            else
278
+            {
279
+                throw new Exception(String.Format("Invalid account information response: {0}", response));
280
+            }
281
+        }
282
+
283
+        public async Task<ProductCatalogue> GetProductCatalogueAsync(Account account)
284
+        {
285
+            var encryptedWarehouseName = Encrypt(account.Warehouse.Name);
286
+            await WriteMessageAsync(new MessageBuilder()
287
+                .Append("Pdt ")
288
+                .Append(encryptedWarehouseName)).ConfigureAwait(false);
289
+            var response = Decrypt(await ReadMessageAsync().ConfigureAwait(false));
290
+            if (response.StartsWith("OK"))
291
+            {
292
+                var parts = response.Split('|');
293
+                var count = int.Parse(parts[1]);
294
+
295
+                var catalogue = new ProductCatalogue();
296
+            
297
+                var listCommand = new MessageBuilder().Append("List ")
298
+                    .Append(encryptedWarehouseName).GetBytes();
299
+                for (var i = 0; i < count; i++)
300
+                {
301
+                    await _connectionStream.WriteAsync(listCommand, 0, listCommand.Length).ConfigureAwait(false);
302
+                    response = Decrypt(await ReadMessageAsync().ConfigureAwait(false));
303
+                    if (response.StartsWith("OK"))
304
+                    {
305
+                        parts = response.Split('|');
306
+                        int networkId = int.Parse(parts[4]);
307
+                        Network network;
308
+                        if (! catalogue.NetworkMap.TryGetValue(networkId, out network))
309
+                        {
310
+                            network = catalogue.AddNetwork(networkId, parts[5]);
311
+                        }
312
+
313
+                        catalogue.AddProduct(
314
+                            network: network,
315
+                            id: int.Parse(parts[1]),
316
+                            faceValue: decimal.Parse(parts[2]),
317
+                            description: parts[3],
318
+                            voucherType: (Batch.Vouchertype)int.Parse(parts[6]),
319
+                            discountPercentage: decimal.Parse(parts[7])
320
+                        );
321
+                    }
322
+                    else
323
+                    {
324
+                        throw new Exception(String.Format("Invalid product item response: {0}", response));
325
+                    }
326
+                }
327
+
328
+                return catalogue;
329
+            }
330
+            else
331
+            {
332
+                throw new Exception(String.Format("Invalid product catalogue response: {0}", response));
333
+            }
334
+        }
335
+
336
+        public async Task<OrderResponse> PlaceOrderAsync(int accountId, Product product, int quantity,
337
+            string customerReference, byte[] key)
338
+        {
339
+            if (key.Length != 24)
340
+            {
341
+                throw new ArgumentException("24 byte key expected", nameof(key));
342
+            }
343
+
344
+            await WriteMessageAsync(new MessageBuilder()
345
+                .Append("Order ")
346
+                .Append(Encrypt(new StringBuilder()
347
+                    .Append(product.Id)
348
+                    .Append("|")
349
+                    .Append(quantity)
350
+                    .Append("|")
351
+                    .Append(customerReference)
352
+                    .Append("|2|") // EncType: 0:None, 1:DES, 2:Triple DES
353
+                    .Append(BitConverter.ToString(key, 0, 8).Replace("-", ""))
354
+                    .Append("|")
355
+                    .Append(BitConverter.ToString(key, 8, 8).Replace("-", ""))
356
+                    .Append("|")
357
+                    .Append(BitConverter.ToString(key, 16, 8).Replace("-", ""))
358
+                    .ToString()))).ConfigureAwait(false);
359
+
360
+            var response = Decrypt(await ReadMessageAsync().ConfigureAwait(false));
361
+
362
+            if (response.StartsWith("OK"))
363
+            {
364
+                var parts = response.Split('|');
365
+                return new OrderResponse()
366
+                {
367
+                    Batch = new Batch()
368
+                    {
369
+                        Id = int.Parse(parts[1]),
370
+                        OrderReference = parts[2],
371
+                        RequestedQuantity = int.Parse(parts[3]),
372
+                        DeliveredQuantity = int.Parse(parts[4]),
373
+                        Cost = decimal.Parse(parts[5]),
374
+                        AccountId = accountId,
375
+                        VendorId = _vendorId,
376
+                        ProductId = product.Id,
377
+                        ProductDescription = product.Description,
378
+                        VoucherType = product.VoucherType,
379
+                        FaceValue = product.FaceValue,
380
+                        DiscountPercentage = product.DiscountPercentage,
381
+                        NetworkId = product.Network.Id,
382
+                        NetworkName = product.Network.Name,
383
+                        OrderDate = DateTime.Now,
384
+                        OrderedById = _userId,
385
+                        ReadyForDownload = false
386
+                    },
387
+                    RemainingBalance = decimal.Parse(parts[6])
388
+                };
389
+            }
390
+            else
391
+            {
392
+                throw new Exception(string.Format("Invalid order response: {0}", response));
393
+            }
394
+        }
395
+
286 396
         private async Task<byte[]> ReadBytesAsync(int count)
287 397
         {
288 398
             int totalBytesRead = 0;
289 399
             byte[] buffer = new byte[count];
290 400
             while (totalBytesRead < count)
291 401
             {
292
-                int bytesRead = await connectionStream.ReadAsync(buffer, totalBytesRead, count - totalBytesRead).ConfigureAwait(false);
402
+                int bytesRead = await _connectionStream.ReadAsync(buffer, totalBytesRead, count - totalBytesRead).ConfigureAwait(false);
293 403
                 if (bytesRead == 0)
294 404
                     throw new Exception("Connection closed unexpectedly");
295 405
                 totalBytesRead += bytesRead;
@@ -301,6 +411,10 @@ namespace MAX
301 411
         {
302 412
             byte[] buffer = await ReadBytesAsync(2).ConfigureAwait(false);
303 413
             int size = buffer[0] * 256 + buffer[1];
414
+            if (size <= 0)
415
+            {
416
+                throw new Exception("Invalid message size");
417
+            }
304 418
             return Encoding.ASCII.GetString(await ReadBytesAsync(size).ConfigureAwait(false));
305 419
         }
306 420
 
@@ -308,21 +422,10 @@ namespace MAX
308 422
 
309 423
         public int SendTimeout { get; set; }
310 424
 
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 425
         private async Task WriteMessageAsync(MessageBuilder message)
323 426
         {
324 427
             byte[] data = message.GetBytes();
325
-            await connectionStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
428
+            await _connectionStream.WriteAsync(data, 0, data.Length).ConfigureAwait(false);
326 429
         }
327 430
     }
328 431
 }

+ 16 - 7
BulkPrintingAPI/Services/MAXClientFactory.cs

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

+ 24 - 0
MAXClient/LoginCredentials.cs

@@ -0,0 +1,24 @@
1
+using MAX.Models;
2
+
3
+namespace MAX
4
+{
5
+    public class LoginCredentials
6
+    {
7
+        public User User { get; set; }
8
+
9
+        public Vendor Vendor { get; set; }
10
+
11
+        public string Password { get; set; }
12
+
13
+        public override string ToString()
14
+        {
15
+            return Format(User.Id, User.Username, Vendor.Id, Vendor.SerialNumber);
16
+        }
17
+
18
+        public static string Format(int userId, string username, int vendorId, string serialNumber)
19
+        {
20
+            return string.Format("userId={0} username={1} vendorId={2} serialNumber={3}",
21
+                userId, username, vendorId, serialNumber);
22
+        }
23
+    }
24
+}

+ 3 - 0
MAXClient/MAXClient.csproj

@@ -5,7 +5,10 @@
5 5
   </PropertyGroup>
6 6
 
7 7
   <ItemGroup>
8
+    <PackageReference Include="Microsoft.Extensions.Configuration" Version="1.1.2" />
9
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="1.1.2" />
8 10
     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="1.1.2" />
11
+    <PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
9 12
     <PackageReference Include="System.Xml.XmlDocument" Version="4.3.0" />
10 13
   </ItemGroup>
11 14
 

+ 8 - 8
MAXClient/MessageBuilder.cs

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

+ 13 - 0
MAXClient/Models/Network.cs

@@ -0,0 +1,13 @@
1
+using System.Collections.Generic;
2
+
3
+namespace MAX.Models
4
+{
5
+    public class Network
6
+    {
7
+        public int Id { get; set; }
8
+
9
+        public string Name { get; set; }
10
+
11
+        public List<Product> Products { get; set; } = new List<Product>();
12
+    }
13
+}

+ 9 - 0
MAXClient/Models/OrderResponse.cs

@@ -0,0 +1,9 @@
1
+namespace MAX.Models
2
+{
3
+    public class OrderResponse
4
+    {
5
+        public Batch Batch { get; set; }
6
+
7
+        public decimal RemainingBalance { get; set; }
8
+    }
9
+}

+ 20 - 0
MAXClient/Models/Product.cs

@@ -0,0 +1,20 @@
1
+using System.Runtime.Serialization;
2
+
3
+namespace MAX.Models
4
+{
5
+    public class Product
6
+    {
7
+        public int Id { get; set; }
8
+
9
+        public Batch.Vouchertype VoucherType { get; set; }
10
+
11
+        public string Description { get; set; }
12
+
13
+        public decimal FaceValue { get; set; }
14
+
15
+        public decimal DiscountPercentage { get; set; }
16
+
17
+        [IgnoreDataMember]
18
+        public Network Network { get; set; }
19
+    }
20
+}

+ 42 - 0
MAXClient/Models/ProductCatalogue.cs

@@ -0,0 +1,42 @@
1
+using System.Collections.Generic;
2
+
3
+namespace MAX.Models
4
+{
5
+    public class ProductCatalogue
6
+    {
7
+        public Network AddNetwork(int id, string name)
8
+        {
9
+            var network = new Network()
10
+            {
11
+                Id = id,
12
+                Name = name
13
+            };
14
+            Networks.Add(network);
15
+            NetworkMap.Add(id, network);
16
+            return network;
17
+        }
18
+
19
+        public Product AddProduct(Network network, int id, decimal faceValue, string description,
20
+            Batch.Vouchertype voucherType, decimal discountPercentage)
21
+        {
22
+            var product = new Product()
23
+            {
24
+                Id = id,
25
+                Network = network,
26
+                FaceValue = faceValue,
27
+                Description = description,
28
+                VoucherType = voucherType,
29
+                DiscountPercentage = discountPercentage
30
+            };
31
+            network.Products.Add(product);
32
+            ProductMap.Add(id, product);
33
+            return product;
34
+        }
35
+
36
+        public List<Network> Networks { get; set; } = new List<Network>();
37
+
38
+        public Dictionary<int, Network> NetworkMap { get; set; } = new Dictionary<int, Network>();
39
+
40
+        public Dictionary<int, Product> ProductMap { get; set; } = new Dictionary<int, Product>();
41
+    }
42
+}

+ 108 - 0
MAXClient/Utils.cs

@@ -0,0 +1,108 @@
1
+using MAX.Models;
2
+using Microsoft.Extensions.Logging;
3
+using System;
4
+using System.IO;
5
+using System.Security.Cryptography;
6
+using System.Text;
7
+using System.Threading.Tasks;
8
+
9
+namespace MAX
10
+{
11
+    public static class Utils
12
+    {
13
+        public static byte[] Transform(ICryptoTransform transform, byte[] input)
14
+        {
15
+            using (var memoryStream = new MemoryStream())
16
+            using (var cryptoStream = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write))
17
+            {
18
+                cryptoStream.Write(input, 0, input.Length);
19
+                cryptoStream.FlushFinalBlock();
20
+                return memoryStream.ToArray();
21
+            }
22
+        }
23
+
24
+        public static string TripleDESDecrypt(string cipherText, TripleDES des)
25
+        {
26
+            using (var decryptor = des.CreateDecryptor(des.Key, des.IV))
27
+            {
28
+                return Encoding.UTF8.GetString(Utils.Transform(decryptor, Convert.FromBase64String(cipherText)));
29
+            }
30
+        }
31
+
32
+        public static string TripleDESDecrypt(string cipherText, byte[] key)
33
+        {
34
+            using (var des = TripleDES.Create())
35
+            {
36
+                des.Key = key;
37
+                des.IV = new byte[8];
38
+                return TripleDESDecrypt(cipherText, des);
39
+            }
40
+        }
41
+
42
+        public static string TripleDESEncrypt(string plainText, TripleDES des)
43
+        {
44
+            using (var encryptor = des.CreateEncryptor(des.Key, des.IV))
45
+            {
46
+                return Convert.ToBase64String(Utils.Transform(encryptor, Encoding.UTF8.GetBytes(plainText)));
47
+            }
48
+        }
49
+
50
+        public static string TripleDESEncrypt(string plainText, byte[] key)
51
+        {
52
+            using (var des = TripleDES.Create())
53
+            {
54
+                des.Key = key;
55
+                des.IV = new byte[8];
56
+                return TripleDESEncrypt(plainText, des);
57
+            }
58
+        }
59
+
60
+        public static async Task<User> AuthenticateUserAsync(ClientFactory clientFactory,
61
+            ILogger logger, int userId, string username, int vendorId, string serialNumber,
62
+            string password)
63
+        {
64
+            using (var client = clientFactory.GetClient(logger, vendorId, serialNumber,
65
+                userId, username, password))
66
+            {
67
+                var user = await client.ConnectAsync().ConfigureAwait(false);
68
+                if (user != null)
69
+                {
70
+                    user.Account = await client.GetAccountAsync().ConfigureAwait(false);
71
+                }
72
+                return user;
73
+            }
74
+        }
75
+
76
+        public static async Task<ProductCatalogue> GetProductCatalogueAsync(
77
+            ClientFactory clientFactory, ILogger logger, LoginCredentials credentials)
78
+        {
79
+            using (var client = clientFactory.GetClient(logger, credentials))
80
+            {
81
+                var user = await client.ConnectAsync().ConfigureAwait(false);
82
+                if (user == null)
83
+                {
84
+                    throw new Exception(string.Format("Invalid login credentials for {0}",
85
+                        credentials.ToString()));
86
+                }
87
+                return await client.GetProductCatalogueAsync(credentials.User.Account).ConfigureAwait(false);
88
+            }
89
+        }
90
+
91
+        public static async Task<OrderResponse> PlaceOrderAsync(
92
+            ClientFactory clientFactory, ILogger logger, LoginCredentials credentials,
93
+            Product product, int quantity, string customerReference, byte[] key)
94
+        {
95
+            using (var client = clientFactory.GetClient(logger, credentials))
96
+            {
97
+                var user = await client.ConnectAsync().ConfigureAwait(false);
98
+                if (user == null)
99
+                {
100
+                    throw new Exception(string.Format("Invalid login credentials for {0}",
101
+                        credentials.ToString()));
102
+                }
103
+                return await client.PlaceOrderAsync(credentials.User.AccountId,
104
+                    product, quantity, customerReference, key).ConfigureAwait(false);
105
+            }
106
+        }
107
+    }
108
+}

+ 1 - 0
MAXData/MAXModels.csproj

@@ -6,6 +6,7 @@
6 6
 
7 7
   <ItemGroup>
8 8
     <PackageReference Include="Microsoft.EntityFrameworkCore" Version="1.1.2" />
9
+    <PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
9 10
   </ItemGroup>
10 11
 
11 12
 </Project>

+ 4 - 4
MAXData/Models/Account.cs

@@ -17,21 +17,21 @@ namespace MAX.Models
17 17
         [DatabaseGenerated(DatabaseGeneratedOption.None)]
18 18
         public int Id { get; set; }
19 19
 
20
-        [MaxLength(50)]
20
+        [Required, MaxLength(50)]
21 21
         public string Name { get; set; }
22 22
 
23 23
         public AccountStatus Status { get; set; }
24 24
 
25
-        [MaxLength(50)]
25
+        [Required, MaxLength(50)]
26 26
         public string Reference { get; set; }
27 27
 
28
-        public decimal Balance { get; set; }
29
-
30 28
         public int WarehouseId { get; set; }
31 29
         public Warehouse Warehouse { get; set; }
32 30
 
33 31
         public ICollection<User> Users { get; set; }
34 32
 
35 33
         public ICollection<Vendor> Vendors { get; set; }
34
+
35
+        public decimal Balance { get; set; }
36 36
     }
37 37
 }

+ 73 - 0
MAXData/Models/Batch.cs

@@ -0,0 +1,73 @@
1
+using System;
2
+using System.Collections.Generic;
3
+using System.ComponentModel.DataAnnotations;
4
+using System.ComponentModel.DataAnnotations.Schema;
5
+using System.Runtime.Serialization;
6
+
7
+namespace MAX.Models
8
+{
9
+    public class Batch
10
+    {
11
+        public enum Vouchertype
12
+        {
13
+            Voucher = 1,
14
+            SMS,
15
+            Data
16
+        }
17
+
18
+        [DatabaseGenerated(DatabaseGeneratedOption.None)]
19
+        public int Id { get; set; }
20
+
21
+        [IgnoreDataMember]
22
+        public int AccountId { get; set; }
23
+
24
+        [IgnoreDataMember]
25
+        public Account Account { get; set; }
26
+
27
+        [IgnoreDataMember]
28
+        public int VendorId { get; set; }
29
+
30
+        // Results in a foreign key clash
31
+        //[IgnoreDataMember]
32
+        //public Vendor Vendor { get; set; }
33
+
34
+        [IgnoreDataMember]
35
+        public int OrderedById { get; set; }
36
+
37
+        // Results in a foreign key clash
38
+        //[IgnoreDataMember]
39
+        //public User OrderedBy { get; set; }
40
+
41
+        public DateTime OrderDate { get; set; }
42
+
43
+        [Required, MaxLength(20)]
44
+        public string OrderReference { get; set; }
45
+
46
+        public int NetworkId { get; set; }
47
+
48
+        [Required, MaxLength(20)]
49
+        public string NetworkName { get; set; }
50
+
51
+        public int ProductId { get; set; }
52
+
53
+        [Required, MaxLength(50)]
54
+        public string ProductDescription { get; set; }
55
+
56
+        public Vouchertype VoucherType { get; set; }
57
+
58
+        public decimal FaceValue { get; set; }
59
+
60
+        public decimal DiscountPercentage { get; set; }
61
+
62
+        public int RequestedQuantity { get; set; }
63
+
64
+        public int DeliveredQuantity { get; set; }
65
+
66
+        public decimal Cost { get; set; }
67
+
68
+        public bool ReadyForDownload { get; set; }
69
+
70
+        [IgnoreDataMember]
71
+        public ICollection<Voucher> Vouchers { get; set; }
72
+    }
73
+}

+ 34 - 0
MAXData/Models/MAXContext.cs

@@ -0,0 +1,34 @@
1
+using Microsoft.EntityFrameworkCore;
2
+using Microsoft.EntityFrameworkCore.Metadata;
3
+
4
+namespace MAX.Models
5
+{
6
+    public class MAXContext : DbContext
7
+    {
8
+        public MAXContext(DbContextOptions<MAXContext> options) : base(options) { }
9
+
10
+        protected override void OnModelCreating(ModelBuilder modelBuilder)
11
+        {
12
+            base.OnModelCreating(modelBuilder);
13
+
14
+            modelBuilder.Entity<Batch>()
15
+                .HasIndex(b => new { b.ReadyForDownload, b.OrderDate });
16
+
17
+            modelBuilder.Entity<Voucher>()
18
+                .HasIndex(v => new { v.BatchId, v.SequenceNumber })
19
+                .IsUnique();
20
+        }
21
+
22
+        public DbSet<Account> Accounts { get; set;  }
23
+
24
+        public DbSet<Batch> Batches { get; set; }
25
+
26
+        public DbSet<User> Users { get; set; }
27
+
28
+        public DbSet<Vendor> Vendors { get; set; }
29
+
30
+        public DbSet<Voucher> Vouchers { get; set; }
31
+
32
+        public DbSet<Warehouse> Warehouses { get; set;  }
33
+    }
34
+}

+ 0 - 14
MAXData/Models/MAXDbContext.cs

@@ -1,14 +0,0 @@
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
-}

+ 6 - 3
MAXData/Models/User.cs

@@ -1,6 +1,7 @@
1 1
 using System;
2 2
 using System.ComponentModel.DataAnnotations;
3 3
 using System.ComponentModel.DataAnnotations.Schema;
4
+using System.Runtime.Serialization;
4 5
 
5 6
 namespace MAX.Models
6 7
 {
@@ -16,16 +17,18 @@ namespace MAX.Models
16 17
         [DatabaseGenerated(DatabaseGeneratedOption.None)]
17 18
         public int Id { get; set; }
18 19
 
19
-        [MaxLength(50)]
20
+        [Required, MaxLength(50)]
20 21
         public string Username { get; set; }
21 22
 
22
-        [MaxLength(50)]
23
+        [Required, MaxLength(50)]
23 24
         public string FirstName { get; set; }
24 25
 
25
-        [MaxLength(50)]
26
+        [Required, MaxLength(50)]
26 27
         public string Surname { get; set; }
27 28
 
29
+        [IgnoreDataMember]
28 30
         public int AccountId { get; set; }
31
+
29 32
         public Account Account { get; set; }
30 33
 
31 34
         public bool Enabled { get; set; }

+ 12 - 1
MAXData/Models/Vendor.cs

@@ -1,5 +1,6 @@
1 1
 using System.ComponentModel.DataAnnotations;
2 2
 using System.ComponentModel.DataAnnotations.Schema;
3
+using System.Runtime.Serialization;
3 4
 
4 5
 namespace MAX.Models
5 6
 {
@@ -8,10 +9,20 @@ namespace MAX.Models
8 9
         [DatabaseGenerated(DatabaseGeneratedOption.None)]
9 10
         public int Id { get; set; }
10 11
 
11
-        [MaxLength(50)]
12
+        [Required, MaxLength(50)]
12 13
         public string SerialNumber { get; set; }
13 14
 
14 15
         public int AccountId { get; set; }
16
+
17
+        [IgnoreDataMember]
15 18
         public Account Account { get; set; }
19
+
20
+        [IgnoreDataMember]
21
+        [Required, MaxLength(32)]
22
+        public byte[] EncryptedDatabasePassword { get; set; }
23
+
24
+        [IgnoreDataMember]
25
+        [Required, MaxLength(32)]
26
+        public byte[] EncryptedVoucherKey { get; set; }
16 27
     }
17 28
 }

+ 30 - 0
MAXData/Models/Voucher.cs

@@ -0,0 +1,30 @@
1
+using System;
2
+using System.ComponentModel.DataAnnotations;
3
+using System.ComponentModel.DataAnnotations.Schema;
4
+using System.Runtime.Serialization;
5
+
6
+namespace MAX.Models
7
+{
8
+    public class Voucher
9
+    {
10
+        [DatabaseGenerated(DatabaseGeneratedOption.None)]
11
+        public int Id { get; set; }
12
+
13
+        [IgnoreDataMember]
14
+        public int BatchId { get; set; }
15
+
16
+        [IgnoreDataMember]
17
+        public Batch Batch { get; set; }
18
+
19
+        public int SequenceNumber { get; set; }
20
+
21
+        [Column(TypeName = "Date")]
22
+        public DateTime ExpiryDate { get; set; }
23
+        
24
+        [Required, MaxLength(50)]
25
+        public string Serial { get; set; }
26
+
27
+        [Required, MaxLength(50)]
28
+        public string EncryptedPIN { get; set; }
29
+    }
30
+}

+ 1 - 1
MAXData/Models/Warehouse.cs

@@ -9,7 +9,7 @@ namespace MAX.Models
9 9
         [DatabaseGenerated(DatabaseGeneratedOption.None)]
10 10
         public int Id { get; set; }
11 11
 
12
-        [MaxLength(50)]
12
+        [Required, MaxLength(50)]
13 13
         public string Name { get; set; }
14 14
 
15 15
         public ICollection<Account> Accounts { get; set; }