Skip to content

Commit e4bfd70

Browse files
authored
Merge pull request #91 from umbraco/feature/oauth-self-hosted
Feature/oauth self hosted
2 parents 80370a5 + 228c5a8 commit e4bfd70

File tree

69 files changed

+2429
-563
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+2429
-563
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System.Collections.Specialized;
2+
3+
namespace Umbraco.Cms.Integrations.Commerce.Shopify.Configuration
4+
{
5+
public class ShopifyOAuthSettings
6+
{
7+
public ShopifyOAuthSettings() { }
8+
9+
public ShopifyOAuthSettings(NameValueCollection appSettings)
10+
{
11+
ClientId = appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyClientIdKey];
12+
13+
ClientSecret = appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyClientSecretKey];
14+
15+
RedirectUri = appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyRedirectUriKey];
16+
17+
Scopes = appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyScopesKey];
18+
19+
TokenEndpoint = appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyTokenEndpointKey];
20+
}
21+
22+
public string ClientId { get; set; }
23+
24+
public string ClientSecret { get; set; }
25+
26+
public string RedirectUri { get; set; }
27+
28+
public string Scopes { get; set; }
29+
30+
public string TokenEndpoint { get; set; }
31+
}
32+
}

src/Umbraco.Cms.Integrations.Commerce.Shopify/Configuration/ShopifySettings.cs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,20 @@ public ShopifySettings()
1111

1212
public ShopifySettings(NameValueCollection appSettings)
1313
{
14-
ApiVersion = appSettings[Constants.UmbracoCmsIntegrationsCommerceShopifyApiVersion];
15-
Shop = appSettings[Constants.UmbracoCmsIntegrationsCommerceShopifyShop];
16-
AccessToken = appSettings[Constants.UmbracoCmsIntegrationsCommerceShopifyAccessToken];
14+
ApiVersion = appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyApiVersion];
15+
Shop = appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyShop];
16+
AccessToken = appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyAccessToken];
17+
UseUmbracoAuthorization = bool.TryParse(appSettings[Constants.Configuration.UmbracoCmsIntegrationsCommerceShopifyUseUmbracoAuthorizationKey], out var key)
18+
? key
19+
: true;
1720
}
1821

1922
public string ApiVersion { get; set; }
2023

2124
public string Shop { get; set; }
2225

2326
public string AccessToken { get; set; }
27+
28+
public bool UseUmbracoAuthorization { get; set; }
2429
}
2530
}

src/Umbraco.Cms.Integrations.Commerce.Shopify/Constants.cs

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,11 @@ namespace Umbraco.Cms.Integrations.Commerce.Shopify
33
{
44
public static class Constants
55
{
6-
public const string UmbracoCmsIntegrationsCommerceShopifyApiVersion =
7-
"Umbraco.Cms.Integrations.Commerce.Shopify.ApiVersion";
8-
9-
public const string UmbracoCmsIntegrationsCommerceShopifyShop =
10-
"Umbraco.Cms.Integrations.Commerce.Shopify.Shop";
6+
public const string AppPluginFolderPath = "~/App_Plugins/UmbracoCms.Integrations/Commerce/Shopify";
117

12-
public const string UmbracoCmsIntegrationsCommerceShopifyAccessToken =
13-
"Umbraco.Cms.Integrations.Commerce.Shopify.AccessToken";
8+
public const string AccessTokenDbKey = "Umbraco.Cms.Integrations.Shopify.AccessTokenDbKey";
149

15-
public const string AppPluginFolderPath = "~/App_Plugins/UmbracoCms.Integrations/Commerce/Shopify";
10+
public const string ProductsApiEndpoint = "https://{0}.myshopify.com/admin/api/{1}/products.json";
1611

1712
public static class RenderingComponent
1813
{
@@ -24,6 +19,30 @@ public static class RenderingComponent
2419
public static class Configuration
2520
{
2621
public const string Settings = "Umbraco:Cms:Integrations:Commerce:Shopify:Settings";
22+
23+
public const string OAuthSettings = "Umbraco:Cms:Integrations:Commerce:Shopify:OAuthSettings";
24+
25+
public const string UmbracoCmsIntegrationsCommerceShopifyApiVersion =
26+
"Umbraco.Cms.Integrations.Commerce.Shopify.ApiVersion";
27+
28+
public const string UmbracoCmsIntegrationsCommerceShopifyShop =
29+
"Umbraco.Cms.Integrations.Commerce.Shopify.Shop";
30+
31+
public const string UmbracoCmsIntegrationsCommerceShopifyAccessToken =
32+
"Umbraco.Cms.Integrations.Commerce.Shopify.AccessToken";
33+
34+
public const string UmbracoCmsIntegrationsCommerceShopifyUseUmbracoAuthorizationKey =
35+
"Umbraco.Cms.Integrations.Commerce.Shopify.UseUmbracoAuthorization";
36+
37+
public const string UmbracoCmsIntegrationsCommerceShopifyClientIdKey = "Umbraco.Cms.Integrations.Commerce.Shopify.ClientId";
38+
39+
public const string UmbracoCmsIntegrationsCommerceShopifyClientSecretKey = "Umbraco.Cms.Integrations.Commerce.Shopify.ClientSecret";
40+
41+
public const string UmbracoCmsIntegrationsCommerceShopifyRedirectUriKey = "Umbraco.Cms.Integrations.Commerce.Shopify.RedirectUri";
42+
43+
public const string UmbracoCmsIntegrationsCommerceShopifyScopesKey = "Umbraco.Cms.Integrations.Commerce.Shopify.Scopes";
44+
45+
public const string UmbracoCmsIntegrationsCommerceShopifyTokenEndpointKey = "Umbraco.Cms.Integrations.Commerce.Shopify.TokenEndpoint";
2746
}
2847

2948
public static class PropertyEditors

src/Umbraco.Cms.Integrations.Commerce.Shopify/Controllers/ProductsController.cs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,22 @@
11
using System.Threading.Tasks;
2+
23
using Umbraco.Cms.Integrations.Commerce.Shopify.Models;
34
using Umbraco.Cms.Integrations.Commerce.Shopify.Models.Dtos;
45
using Umbraco.Cms.Integrations.Commerce.Shopify.Services;
6+
using Umbraco.Cms.Integrations.Commerce.Shopify.Configuration;
57

8+
using static Umbraco.Cms.Integrations.Commerce.Shopify.ShopifyComposer;
69

710
#if NETCOREAPP
11+
using Microsoft.Extensions.Options;
812
using Microsoft.AspNetCore.Mvc;
913

1014
using Umbraco.Cms.Web.BackOffice.Controllers;
1115
using Umbraco.Cms.Web.Common.Attributes;
1216
#else
17+
using System.Configuration;
1318
using System.Web.Http;
19+
1420
using Umbraco.Web.Mvc;
1521
using Umbraco.Web.WebApi;
1622
#endif
@@ -20,21 +26,36 @@ namespace Umbraco.Cms.Integrations.Commerce.Shopify.Controllers
2026
[PluginController("UmbracoCmsIntegrationsCommerceShopify")]
2127
public class ProductsController : UmbracoAuthorizedApiController
2228
{
29+
private readonly ShopifySettings _settings;
30+
31+
private readonly IShopifyAuthorizationService _authorizationService;
32+
2333
private readonly IShopifyService _apiService;
2434

25-
public ProductsController(IShopifyService apiService)
35+
#if NETCOREAPP
36+
public ProductsController(IOptions<ShopifySettings> options, IShopifyService apiService, AuthorizationImplementationFactory authorizationImplementationFactory)
37+
#else
38+
public ProductsController(IShopifyService apiService, AuthorizationImplementationFactory authorizationImplementationFactory)
39+
#endif
2640
{
41+
#if NETCOREAPP
42+
_settings = options.Value;
43+
#else
44+
_settings = new ShopifySettings(ConfigurationManager.AppSettings);
45+
#endif
2746
_apiService = apiService;
47+
_authorizationService = authorizationImplementationFactory(_settings.UseUmbracoAuthorization);
2848
}
2949

3050
[HttpGet]
3151
public EditorSettings CheckConfiguration() => _apiService.GetApiConfiguration();
3252

3353
[HttpGet]
34-
public string GetAuthorizationUrl() => _apiService.GetAuthorizationUrl();
54+
public string GetAuthorizationUrl() => _authorizationService.GetAuthorizationUrl();
3555

3656
[HttpPost]
37-
public async Task<string> GetAccessToken([FromBody] OAuthRequestDto authRequestDto) => await _apiService.GetAccessToken(authRequestDto);
57+
public async Task<string> GetAccessToken([FromBody] OAuthRequestDto authRequestDto) =>
58+
await _authorizationService.GetAccessTokenAsync(authRequestDto.Code);
3859

3960
[HttpGet]
4061
public async Task<ResponseDto<ProductsListDto>> ValidateAccessToken() => await _apiService.ValidateAccessToken();
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Umbraco.Cms.Integrations.Commerce.Shopify.Models;
2+
3+
#if NETCOREAPP
4+
using Microsoft.AspNetCore.Mvc;
5+
6+
using Umbraco.Cms.Web.Common.Controllers;
7+
#else
8+
using System.Web.Http;
9+
using System.Net.Http;
10+
using System.Net.Http.Headers;
11+
12+
using Umbraco.Web.WebApi;
13+
#endif
14+
15+
namespace Umbraco.Cms.Integrations.Commerce.Shopify.Controllers
16+
{
17+
public class ShopifyAuthorizationController : UmbracoApiController
18+
{
19+
[HttpGet]
20+
#if NETCOREAPP
21+
public IActionResult OAuth(string code)
22+
{
23+
return new ContentResult
24+
{
25+
Content = string.IsNullOrEmpty(code)
26+
? JavascriptResponse.Fail("Authorization process failed.")
27+
: JavascriptResponse.Ok(code),
28+
ContentType = "text/html"
29+
};
30+
}
31+
#else
32+
public HttpResponseMessage OAuth(string code)
33+
{
34+
var response = new HttpResponseMessage();
35+
response.Content = new StringContent(string.IsNullOrEmpty(code)
36+
? JavascriptResponse.Fail("Authorization process failed.")
37+
: JavascriptResponse.Ok(code));
38+
response.Content.Headers.ContentType = new MediaTypeHeaderValue("text/html");
39+
return response;
40+
}
41+
#endif
42+
}
43+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Umbraco.Cms.Integrations.Commerce.Shopify.Models
8+
{
9+
public class JavascriptResponse
10+
{
11+
protected StringBuilder ScriptBuilder { get; set; }
12+
13+
public bool Success { get; set; }
14+
15+
public string Error { get; set; }
16+
17+
public bool Failure => !Success;
18+
19+
protected JavascriptResponse(bool success, string data, string error)
20+
{
21+
if (success && !string.IsNullOrEmpty(error))
22+
{
23+
throw new ArgumentException("A succesful Response cannot have an error message.", error);
24+
}
25+
26+
if (!success && string.IsNullOrEmpty(error))
27+
{
28+
throw new ArgumentException("A failure Response must have an error message.", error);
29+
}
30+
31+
Success = success;
32+
33+
Error = error;
34+
35+
ScriptBuilder = new StringBuilder();
36+
ScriptBuilder.AppendLine("<script>");
37+
ScriptBuilder.AppendLine(success
38+
? "window.opener.postMessage({ type: 'shopify:oauth:success', url: location.href, code: '" + data + "' }, '*');"
39+
: "window.opener.postMessage({ type: 'shopify:oauth:error', url: location.href, response: '" + error + "' }, '*');");
40+
ScriptBuilder.AppendLine("window.close();");
41+
ScriptBuilder.AppendLine("</script>");
42+
}
43+
44+
public override string ToString() => ScriptBuilder.ToString();
45+
46+
public static string Ok(string data) => new JavascriptResponse(true, data, string.Empty).ToString();
47+
48+
public static string Fail(string error) => new JavascriptResponse(false, string.Empty, error).ToString();
49+
}
50+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#if NETCOREAPP
2+
using Microsoft.Extensions.Options;
3+
#else
4+
using System.Configuration;
5+
#endif
6+
7+
using System;
8+
using System.Collections.Generic;
9+
using System.Net;
10+
using System.Net.Http;
11+
using System.Threading.Tasks;
12+
13+
using Umbraco.Cms.Integrations.Commerce.Shopify.Configuration;
14+
using Umbraco.Cms.Integrations.Commerce.Shopify.Models.Dtos;
15+
16+
using Newtonsoft.Json;
17+
18+
namespace Umbraco.Cms.Integrations.Commerce.Shopify.Services
19+
{
20+
public class AuthorizationService : BaseAuthorizationService, IShopifyAuthorizationService
21+
{
22+
private readonly ShopifySettings _settings;
23+
24+
private readonly ShopifyOAuthSettings _oauthSettings;
25+
26+
#if NETCOREAPP
27+
public AuthorizationService(IOptions<ShopifySettings> options, IOptions<ShopifyOAuthSettings> oauthSettings, ITokenService tokenService)
28+
#else
29+
public AuthorizationService(ITokenService tokenService)
30+
#endif
31+
: base(tokenService)
32+
{
33+
#if NETCOREAPP
34+
_settings = options.Value;
35+
_oauthSettings = oauthSettings.Value;
36+
#else
37+
_settings = new ShopifySettings(ConfigurationManager.AppSettings);
38+
_oauthSettings = new ShopifyOAuthSettings(ConfigurationManager.AppSettings);
39+
#endif
40+
}
41+
42+
public string GetAccessToken(string code) =>
43+
GetAccessTokenAsync(code).ConfigureAwait(false).GetAwaiter().GetResult();
44+
45+
public async Task<string> GetAccessTokenAsync(string code)
46+
{
47+
var data = new Dictionary<string, string>
48+
{
49+
{ "client_id", _oauthSettings.ClientId },
50+
{ "client_secret", _oauthSettings.ClientSecret },
51+
{ "redirect_uri", _oauthSettings.RedirectUri },
52+
{ "code", code }
53+
};
54+
55+
var requestMessage = new HttpRequestMessage
56+
{
57+
Method = HttpMethod.Post,
58+
RequestUri = new Uri(_oauthSettings.TokenEndpoint),
59+
Content = new FormUrlEncodedContent(data)
60+
};
61+
62+
var response = await ClientFactory().SendAsync(requestMessage);
63+
if (response.IsSuccessStatusCode)
64+
{
65+
var result = await response.Content.ReadAsStringAsync();
66+
67+
var tokenDto = JsonConvert.DeserializeObject<TokenDto>(result);
68+
69+
TokenService.SaveParameters(Constants.AccessTokenDbKey, tokenDto.AccessToken);
70+
71+
return result;
72+
}
73+
74+
if (response.StatusCode == HttpStatusCode.BadRequest)
75+
{
76+
var errorResult = await response.Content.ReadAsStringAsync();
77+
var errorDto = JsonConvert.DeserializeObject<ErrorDto>(errorResult);
78+
79+
return "Error: " + errorDto.Message;
80+
}
81+
82+
return "Error: An unexpected error occurred.";
83+
}
84+
85+
public string GetAuthorizationUrl() =>
86+
string.Format(ShopifyAuthorizationUrl,
87+
_settings.Shop,
88+
_oauthSettings.ClientId,
89+
_oauthSettings.RedirectUri);
90+
}
91+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System;
2+
using System.Net.Http;
3+
4+
namespace Umbraco.Cms.Integrations.Commerce.Shopify.Services
5+
{
6+
public class BaseAuthorizationService
7+
{
8+
// Using a static HttpClient (see: https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/).
9+
private readonly static HttpClient s_client = new HttpClient();
10+
11+
// Access to the client within the class is via ClientFactory(), allowing us to mock the responses in tests.
12+
public static Func<HttpClient> ClientFactory = () => s_client;
13+
14+
protected readonly ITokenService TokenService;
15+
16+
protected const string ShopifyAuthorizationUrl = "https://{0}.myshopify.com/admin/oauth/authorize" +
17+
"?client_id={1}" +
18+
"&redirect_uri={2}" +
19+
"&scope=read_products" +
20+
"&grant_options[]=value";
21+
22+
public BaseAuthorizationService(ITokenService tokenService)
23+
{
24+
TokenService = tokenService;
25+
}
26+
}
27+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Threading.Tasks;
2+
3+
namespace Umbraco.Cms.Integrations.Commerce.Shopify.Services
4+
{
5+
public interface IShopifyAuthorizationService
6+
{
7+
string GetAuthorizationUrl();
8+
9+
string GetAccessToken(string code);
10+
11+
Task<string> GetAccessTokenAsync(string code);
12+
}
13+
}

0 commit comments

Comments
 (0)