diff --git a/Notesnook.API/Controllers/MonographsController.cs b/Notesnook.API/Controllers/MonographsController.cs index c9bd312..25ec93d 100644 --- a/Notesnook.API/Controllers/MonographsController.cs +++ b/Notesnook.API/Controllers/MonographsController.cs @@ -35,6 +35,7 @@ using Notesnook.API.Authorization; using Notesnook.API.Models; using Notesnook.API.Services; using Streetwriters.Common; +using Streetwriters.Common.Helpers; using Streetwriters.Common.Interfaces; using Streetwriters.Common.Messages; using Streetwriters.Data.Interfaces; @@ -272,18 +273,20 @@ namespace Notesnook.API.Controllers return Content(SVG_PIXEL, "image/svg+xml"); } - [HttpGet("{id}/stats")] - public async Task GetMonographStatsAsync([FromRoute] string id) + [HttpGet("{id}/analytics")] + public async Task GetMonographAnalyticsAsync([FromRoute] string id) { + if (!FeatureAuthorizationHelper.IsFeatureAllowed(Features.MONOGRAPH_ANALYTICS, Clients.Notesnook.Id, User)) + return BadRequest(new { error = "Monograph analytics are only available on the Pro & Believer plans." }); + var userId = this.User.GetUserId(); var monograph = await FindMonographAsync(id); - if (monograph == null || monograph.Deleted || monograph.UserId != userId) { return NotFound(); } - return Ok(new { viewCount = monograph.ViewCount }); + return Ok(new { totalViews = monograph.ViewCount }); } [HttpDelete("{id}")] diff --git a/Streetwriters.Common/Helpers/FeatureAuthorizationHelper.cs b/Streetwriters.Common/Helpers/FeatureAuthorizationHelper.cs new file mode 100644 index 0000000..d77e636 --- /dev/null +++ b/Streetwriters.Common/Helpers/FeatureAuthorizationHelper.cs @@ -0,0 +1,60 @@ +using System.IO; +using System.Security.Claims; +using System.Threading.Tasks; +using Streetwriters.Common.Enums; +using Streetwriters.Common.Interfaces; +using WebMarkupMin.Core; +using WebMarkupMin.Core.Loggers; + +namespace Streetwriters.Common.Helpers +{ + public enum Features + { + SMS_2FA, + MONOGRAPH_ANALYTICS + } + + public static class FeatureAuthorizationHelper + { + private static SubscriptionPlan? GetUserSubscriptionPlan(string clientId, ClaimsPrincipal user) + { + var claimKey = $"{clientId}:status"; + var status = user.FindFirstValue(claimKey); + switch (status) + { + case "free": + return SubscriptionPlan.FREE; + case "believer": + return SubscriptionPlan.BELIEVER; + case "education": + return SubscriptionPlan.EDUCATION; + case "essential": + return SubscriptionPlan.ESSENTIAL; + case "pro": + return SubscriptionPlan.PRO; + default: + return null; + } + } + + public static bool IsFeatureAllowed(Features feature, string clientId, ClaimsPrincipal user) + { + if (Constants.IS_SELF_HOSTED) + return true; + + var status = GetUserSubscriptionPlan(clientId, user); + + switch (feature) + { + case Features.SMS_2FA: + case Features.MONOGRAPH_ANALYTICS: + return status == SubscriptionPlan.LEGACY_PRO || + status == SubscriptionPlan.PRO || + status == SubscriptionPlan.EDUCATION || + status == SubscriptionPlan.BELIEVER; + default: + return false; + } + } + } +} diff --git a/Streetwriters.Identity/Controllers/MFAController.cs b/Streetwriters.Identity/Controllers/MFAController.cs index 92200ce..73d7da4 100644 --- a/Streetwriters.Identity/Controllers/MFAController.cs +++ b/Streetwriters.Identity/Controllers/MFAController.cs @@ -29,6 +29,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Streetwriters.Common; using Streetwriters.Common.Enums; +using Streetwriters.Common.Helpers; using Streetwriters.Common.Models; using Streetwriters.Identity.Interfaces; using Streetwriters.Identity.Models; @@ -53,6 +54,9 @@ namespace Streetwriters.Identity.Controllers var user = await UserManager.GetUserAsync(User) ?? throw new Exception("User not found."); + if (form.Type == MFAMethods.SMS && !FeatureAuthorizationHelper.IsFeatureAllowed(Features.SMS_2FA, client.Id, User)) + throw new Exception("2FA via SMS is only available on Pro & Believer plans."); + try { switch (form.Type) @@ -62,7 +66,7 @@ namespace Streetwriters.Identity.Controllers return Ok(authenticatorDetails); case "sms": case "email": - await MFAService.SendOTPAsync(user, client, form, true); + await MFAService.SendOTPAsync(user, client, form); return Ok(); default: return BadRequest("Invalid authenticator type."); diff --git a/Streetwriters.Identity/Interfaces/IMFAService.cs b/Streetwriters.Identity/Interfaces/IMFAService.cs index d5f448e..d2cac2a 100644 --- a/Streetwriters.Identity/Interfaces/IMFAService.cs +++ b/Streetwriters.Identity/Interfaces/IMFAService.cs @@ -36,7 +36,7 @@ namespace Streetwriters.Identity.Interfaces bool IsValidMFAMethod(string method); bool IsValidMFAMethod(string method, User user); Task GetAuthenticatorDetailsAsync(User user, IClient client); - Task SendOTPAsync(User user, IClient client, MultiFactorSetupForm form, bool isSetup = false); + Task SendOTPAsync(User user, IClient client, MultiFactorSetupForm form); Task VerifyOTPAsync(User user, string code, string method); } } \ No newline at end of file diff --git a/Streetwriters.Identity/Services/MFAService.cs b/Streetwriters.Identity/Services/MFAService.cs index 021f4a0..ce972cc 100644 --- a/Streetwriters.Identity/Services/MFAService.cs +++ b/Streetwriters.Identity/Services/MFAService.cs @@ -25,7 +25,6 @@ using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Logging; -using Streetwriters.Common; using Streetwriters.Common.Enums; using Streetwriters.Common.Interfaces; using Streetwriters.Common.Models; @@ -168,18 +167,12 @@ namespace Streetwriters.Identity.Services }; } - public async Task SendOTPAsync(User user, IClient client, MultiFactorSetupForm form, bool isSetup = false) + public async Task SendOTPAsync(User user, IClient client, MultiFactorSetupForm form) { var method = form.Type; if ((method != MFAMethods.Email && method != MFAMethods.SMS) || !IsValidMFAMethod(method)) throw new Exception("Invalid method."); - if (isSetup && - method == MFAMethods.SMS && - !UserService.IsSMSMFAAllowed(client.Id, user)) - throw new Exception("Due to the high costs of SMS, 2FA via SMS is only available on Pro & Believer plans."); - - // if (!user.EmailConfirmed) throw new Exception("Please confirm your email before activating 2FA by email."); await GetAuthenticatorDetailsAsync(user, client); switch (method) diff --git a/Streetwriters.Identity/Services/UserService.cs b/Streetwriters.Identity/Services/UserService.cs index 7882ba6..4b34c1f 100644 --- a/Streetwriters.Identity/Services/UserService.cs +++ b/Streetwriters.Identity/Services/UserService.cs @@ -29,37 +29,6 @@ namespace Streetwriters.Identity.Services { public class UserService { - private static SubscriptionPlan? GetUserSubscriptionPlan(string clientId, User user) - { - var claimKey = GetClaimKey(clientId); - var status = user.Claims.FirstOrDefault((c) => c.ClaimType == claimKey)?.ClaimValue; - switch (status) - { - case "free": - return SubscriptionPlan.FREE; - case "believer": - return SubscriptionPlan.BELIEVER; - case "education": - return SubscriptionPlan.EDUCATION; - case "essential": - return SubscriptionPlan.ESSENTIAL; - case "pro": - return SubscriptionPlan.PRO; - default: - return null; - } - } - - public static bool IsSMSMFAAllowed(string clientId, User user) - { - var status = GetUserSubscriptionPlan(clientId, user); - if (status == null) return false; - return status == SubscriptionPlan.LEGACY_PRO || - status == SubscriptionPlan.PRO || - status == SubscriptionPlan.EDUCATION || - status == SubscriptionPlan.BELIEVER; - } - public static Claim SubscriptionPlanToClaim(string clientId, Subscription subscription) { var claimKey = GetClaimKey(clientId);