Phase 5B Project Subscription Infrastructure
This commit is contained in:
parent
90b2212890
commit
49f239b301
@ -12,7 +12,8 @@ public sealed class ManageAccountController(
|
|||||||
ICurrentUserService currentUser,
|
ICurrentUserService currentUser,
|
||||||
IUserRepository users,
|
IUserRepository users,
|
||||||
IAuthService authService,
|
IAuthService authService,
|
||||||
ITwoFactorService twoFactorService) : Controller
|
ITwoFactorService twoFactorService,
|
||||||
|
ISubscriptionService subscriptions) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
@ -23,7 +24,11 @@ public sealed class ManageAccountController(
|
|||||||
return RedirectToAction("Login", "Account");
|
return RedirectToAction("Login", "Account");
|
||||||
}
|
}
|
||||||
|
|
||||||
return View(user);
|
return View(new ManageAccountViewModel
|
||||||
|
{
|
||||||
|
User = user,
|
||||||
|
Subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
|
|||||||
42
PlotLine/Data/SubscriptionRepository.cs
Normal file
42
PlotLine/Data/SubscriptionRepository.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using System.Data;
|
||||||
|
using Dapper;
|
||||||
|
using PlotLine.Models;
|
||||||
|
|
||||||
|
namespace PlotLine.Data;
|
||||||
|
|
||||||
|
public interface ISubscriptionRepository
|
||||||
|
{
|
||||||
|
Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(int userId);
|
||||||
|
Task<SubscriptionLevel?> GetSubscriptionLevelAsync(int subscriptionLevelId);
|
||||||
|
Task<IReadOnlyList<SubscriptionLevel>> GetActiveSubscriptionLevelsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SubscriptionRepository(ISqlConnectionFactory connectionFactory) : ISubscriptionRepository
|
||||||
|
{
|
||||||
|
public async Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(int userId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<UserSubscriptionDetail>(
|
||||||
|
"dbo.UserSubscription_GetCurrent",
|
||||||
|
new { UserID = userId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SubscriptionLevel?> GetSubscriptionLevelAsync(int subscriptionLevelId)
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
return await connection.QuerySingleOrDefaultAsync<SubscriptionLevel>(
|
||||||
|
"dbo.SubscriptionLevel_Get",
|
||||||
|
new { SubscriptionLevelID = subscriptionLevelId },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<SubscriptionLevel>> GetActiveSubscriptionLevelsAsync()
|
||||||
|
{
|
||||||
|
using var connection = connectionFactory.CreateConnection();
|
||||||
|
var rows = await connection.QueryAsync<SubscriptionLevel>(
|
||||||
|
"dbo.SubscriptionLevel_ListActive",
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rows.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
48
PlotLine/Models/SubscriptionModels.cs
Normal file
48
PlotLine/Models/SubscriptionModels.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
namespace PlotLine.Models;
|
||||||
|
|
||||||
|
public sealed class SubscriptionLevel
|
||||||
|
{
|
||||||
|
public int SubscriptionLevelID { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public int MaxBooks { get; set; }
|
||||||
|
public int MaxChaptersPerBook { get; set; }
|
||||||
|
public int MaxScenesPerBook { get; set; }
|
||||||
|
public int MaxCharactersPerProject { get; set; }
|
||||||
|
public int MaxStorageMB { get; set; }
|
||||||
|
public int MaxCollaborators { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
public DateTime CreatedDateUTC { get; set; }
|
||||||
|
public DateTime ModifiedDateUTC { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserSubscription
|
||||||
|
{
|
||||||
|
public int UserSubscriptionID { get; set; }
|
||||||
|
public int UserID { get; set; }
|
||||||
|
public int SubscriptionLevelID { get; set; }
|
||||||
|
public DateTime StartDateUTC { get; set; }
|
||||||
|
public DateTime? EndDateUTC { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public bool IsTrial { get; set; }
|
||||||
|
public DateTime? TrialExpiryDateUTC { get; set; }
|
||||||
|
public DateTime CreatedDateUTC { get; set; }
|
||||||
|
public DateTime ModifiedDateUTC { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class UserSubscriptionDetail : UserSubscription
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string DisplayName { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public int MaxBooks { get; set; }
|
||||||
|
public int MaxChaptersPerBook { get; set; }
|
||||||
|
public int MaxScenesPerBook { get; set; }
|
||||||
|
public int MaxCharactersPerProject { get; set; }
|
||||||
|
public int MaxStorageMB { get; set; }
|
||||||
|
public int MaxCollaborators { get; set; }
|
||||||
|
public bool SubscriptionLevelIsActive { get; set; }
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
@ -103,6 +103,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IAcceptanceRepository, AcceptanceRepository>();
|
builder.Services.AddScoped<IAcceptanceRepository, AcceptanceRepository>();
|
||||||
builder.Services.AddScoped<IImportRepository, ImportRepository>();
|
builder.Services.AddScoped<IImportRepository, ImportRepository>();
|
||||||
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
||||||
|
builder.Services.AddScoped<ISubscriptionRepository, SubscriptionRepository>();
|
||||||
builder.Services.AddScoped<IProjectService, ProjectService>();
|
builder.Services.AddScoped<IProjectService, ProjectService>();
|
||||||
builder.Services.AddScoped<IBookService, BookService>();
|
builder.Services.AddScoped<IBookService, BookService>();
|
||||||
builder.Services.AddScoped<IChapterService, ChapterService>();
|
builder.Services.AddScoped<IChapterService, ChapterService>();
|
||||||
@ -128,6 +129,7 @@ public class Program
|
|||||||
builder.Services.AddScoped<IAcceptanceService, AcceptanceService>();
|
builder.Services.AddScoped<IAcceptanceService, AcceptanceService>();
|
||||||
builder.Services.AddScoped<IImportService, ImportService>();
|
builder.Services.AddScoped<IImportService, ImportService>();
|
||||||
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
builder.Services.AddScoped<IProjectAccessService, ProjectAccessService>();
|
||||||
|
builder.Services.AddScoped<ISubscriptionService, SubscriptionService>();
|
||||||
builder.Services.AddSingleton<IHelpService, HelpService>();
|
builder.Services.AddSingleton<IHelpService, HelpService>();
|
||||||
builder.Services.AddSingleton<IHelpMarkdownRenderer, HelpMarkdownRenderer>();
|
builder.Services.AddSingleton<IHelpMarkdownRenderer, HelpMarkdownRenderer>();
|
||||||
builder.Services.AddScoped<IAuthService, AuthService>();
|
builder.Services.AddScoped<IAuthService, AuthService>();
|
||||||
|
|||||||
23
PlotLine/Services/SubscriptionServices.cs
Normal file
23
PlotLine/Services/SubscriptionServices.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using PlotLine.Data;
|
||||||
|
using PlotLine.Models;
|
||||||
|
|
||||||
|
namespace PlotLine.Services;
|
||||||
|
|
||||||
|
public interface ISubscriptionService
|
||||||
|
{
|
||||||
|
Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(int userId);
|
||||||
|
Task<SubscriptionLevel?> GetSubscriptionLevelAsync(int subscriptionLevelId);
|
||||||
|
Task<IReadOnlyList<SubscriptionLevel>> GetActiveSubscriptionLevelsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class SubscriptionService(ISubscriptionRepository subscriptions) : ISubscriptionService
|
||||||
|
{
|
||||||
|
public Task<UserSubscriptionDetail?> GetCurrentSubscriptionAsync(int userId) =>
|
||||||
|
subscriptions.GetCurrentSubscriptionAsync(userId);
|
||||||
|
|
||||||
|
public Task<SubscriptionLevel?> GetSubscriptionLevelAsync(int subscriptionLevelId) =>
|
||||||
|
subscriptions.GetSubscriptionLevelAsync(subscriptionLevelId);
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<SubscriptionLevel>> GetActiveSubscriptionLevelsAsync() =>
|
||||||
|
subscriptions.GetActiveSubscriptionLevelsAsync();
|
||||||
|
}
|
||||||
232
PlotLine/Sql/043_Phase5B_SubscriptionInfrastructure.sql
Normal file
232
PlotLine/Sql/043_Phase5B_SubscriptionInfrastructure.sql
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
SET ANSI_NULLS ON;
|
||||||
|
GO
|
||||||
|
SET QUOTED_IDENTIFIER ON;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.SubscriptionLevel', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.SubscriptionLevel
|
||||||
|
(
|
||||||
|
SubscriptionLevelID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_SubscriptionLevel PRIMARY KEY,
|
||||||
|
Name nvarchar(100) NOT NULL,
|
||||||
|
DisplayName nvarchar(100) NOT NULL,
|
||||||
|
Description nvarchar(500) NULL,
|
||||||
|
MaxBooks int NOT NULL,
|
||||||
|
MaxChaptersPerBook int NOT NULL,
|
||||||
|
MaxScenesPerBook int NOT NULL,
|
||||||
|
MaxCharactersPerProject int NOT NULL,
|
||||||
|
MaxStorageMB int NOT NULL,
|
||||||
|
MaxCollaborators int NOT NULL,
|
||||||
|
IsActive bit NOT NULL CONSTRAINT DF_SubscriptionLevel_IsActive DEFAULT 1,
|
||||||
|
SortOrder int NOT NULL,
|
||||||
|
CreatedDateUTC datetime2 NOT NULL CONSTRAINT DF_SubscriptionLevel_CreatedDateUTC DEFAULT SYSUTCDATETIME(),
|
||||||
|
ModifiedDateUTC datetime2 NOT NULL CONSTRAINT DF_SubscriptionLevel_ModifiedDateUTC DEFAULT SYSUTCDATETIME()
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_SubscriptionLevel_Name' AND object_id = OBJECT_ID(N'dbo.SubscriptionLevel'))
|
||||||
|
CREATE UNIQUE INDEX UX_SubscriptionLevel_Name ON dbo.SubscriptionLevel(Name);
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID(N'dbo.UserSubscription', N'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE dbo.UserSubscription
|
||||||
|
(
|
||||||
|
UserSubscriptionID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_UserSubscription PRIMARY KEY,
|
||||||
|
UserID int NOT NULL,
|
||||||
|
SubscriptionLevelID int NOT NULL,
|
||||||
|
StartDateUTC datetime2 NOT NULL CONSTRAINT DF_UserSubscription_StartDateUTC DEFAULT SYSUTCDATETIME(),
|
||||||
|
EndDateUTC datetime2 NULL,
|
||||||
|
IsActive bit NOT NULL CONSTRAINT DF_UserSubscription_IsActive DEFAULT 1,
|
||||||
|
IsTrial bit NOT NULL CONSTRAINT DF_UserSubscription_IsTrial DEFAULT 0,
|
||||||
|
TrialExpiryDateUTC datetime2 NULL,
|
||||||
|
CreatedDateUTC datetime2 NOT NULL CONSTRAINT DF_UserSubscription_CreatedDateUTC DEFAULT SYSUTCDATETIME(),
|
||||||
|
ModifiedDateUTC datetime2 NOT NULL CONSTRAINT DF_UserSubscription_ModifiedDateUTC DEFAULT SYSUTCDATETIME(),
|
||||||
|
CONSTRAINT FK_UserSubscription_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID),
|
||||||
|
CONSTRAINT FK_UserSubscription_SubscriptionLevel FOREIGN KEY (SubscriptionLevelID) REFERENCES dbo.SubscriptionLevel(SubscriptionLevelID)
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_UserSubscription_User_Active' AND object_id = OBJECT_ID(N'dbo.UserSubscription'))
|
||||||
|
CREATE UNIQUE INDEX UX_UserSubscription_User_Active ON dbo.UserSubscription(UserID) WHERE IsActive = 1;
|
||||||
|
GO
|
||||||
|
|
||||||
|
MERGE dbo.SubscriptionLevel AS target
|
||||||
|
USING (VALUES
|
||||||
|
(N'DraftDesk', N'Draft Desk', N'Trial and evaluation.', 2, 75, 300, 75, 100, 0, 1, 10),
|
||||||
|
(N'WritingRoom', N'Writing Room', N'Standard subscription for most authors.', 10, 150, 1000, 250, 1024, 1, 1, 20),
|
||||||
|
(N'StoryForge', N'Story Forge', N'Professional authors and co-authors.', 30, 200, 2000, 500, 5120, 3, 1, 30),
|
||||||
|
(N'AuthorStudio', N'Author Studio', N'Advanced users and writing teams.', 999999, 999999, 999999, 999999, 20480, 10, 1, 40)
|
||||||
|
) AS source
|
||||||
|
(
|
||||||
|
Name,
|
||||||
|
DisplayName,
|
||||||
|
Description,
|
||||||
|
MaxBooks,
|
||||||
|
MaxChaptersPerBook,
|
||||||
|
MaxScenesPerBook,
|
||||||
|
MaxCharactersPerProject,
|
||||||
|
MaxStorageMB,
|
||||||
|
MaxCollaborators,
|
||||||
|
IsActive,
|
||||||
|
SortOrder
|
||||||
|
)
|
||||||
|
ON target.Name = source.Name
|
||||||
|
WHEN MATCHED THEN UPDATE SET
|
||||||
|
DisplayName = source.DisplayName,
|
||||||
|
Description = source.Description,
|
||||||
|
MaxBooks = source.MaxBooks,
|
||||||
|
MaxChaptersPerBook = source.MaxChaptersPerBook,
|
||||||
|
MaxScenesPerBook = source.MaxScenesPerBook,
|
||||||
|
MaxCharactersPerProject = source.MaxCharactersPerProject,
|
||||||
|
MaxStorageMB = source.MaxStorageMB,
|
||||||
|
MaxCollaborators = source.MaxCollaborators,
|
||||||
|
IsActive = source.IsActive,
|
||||||
|
SortOrder = source.SortOrder,
|
||||||
|
ModifiedDateUTC = SYSUTCDATETIME()
|
||||||
|
WHEN NOT MATCHED THEN
|
||||||
|
INSERT
|
||||||
|
(
|
||||||
|
Name,
|
||||||
|
DisplayName,
|
||||||
|
Description,
|
||||||
|
MaxBooks,
|
||||||
|
MaxChaptersPerBook,
|
||||||
|
MaxScenesPerBook,
|
||||||
|
MaxCharactersPerProject,
|
||||||
|
MaxStorageMB,
|
||||||
|
MaxCollaborators,
|
||||||
|
IsActive,
|
||||||
|
SortOrder
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
source.Name,
|
||||||
|
source.DisplayName,
|
||||||
|
source.Description,
|
||||||
|
source.MaxBooks,
|
||||||
|
source.MaxChaptersPerBook,
|
||||||
|
source.MaxScenesPerBook,
|
||||||
|
source.MaxCharactersPerProject,
|
||||||
|
source.MaxStorageMB,
|
||||||
|
source.MaxCollaborators,
|
||||||
|
source.IsActive,
|
||||||
|
source.SortOrder
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
DECLARE @AuthorStudioID int;
|
||||||
|
SELECT @AuthorStudioID = SubscriptionLevelID
|
||||||
|
FROM dbo.SubscriptionLevel
|
||||||
|
WHERE Name = N'AuthorStudio';
|
||||||
|
|
||||||
|
IF @AuthorStudioID IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.UserSubscription (UserID, SubscriptionLevelID, StartDateUTC, IsActive, IsTrial)
|
||||||
|
SELECT u.UserID, @AuthorStudioID, SYSUTCDATETIME(), 1, 0
|
||||||
|
FROM dbo.AppUser u
|
||||||
|
WHERE NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.UserSubscription us
|
||||||
|
WHERE us.UserID = u.UserID
|
||||||
|
AND us.IsActive = 1
|
||||||
|
);
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.SubscriptionLevel_ListActive
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT SubscriptionLevelID, Name, DisplayName, Description, MaxBooks, MaxChaptersPerBook,
|
||||||
|
MaxScenesPerBook, MaxCharactersPerProject, MaxStorageMB, MaxCollaborators, IsActive,
|
||||||
|
SortOrder, CreatedDateUTC, ModifiedDateUTC
|
||||||
|
FROM dbo.SubscriptionLevel
|
||||||
|
WHERE IsActive = 1
|
||||||
|
ORDER BY SortOrder, DisplayName;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.SubscriptionLevel_Get
|
||||||
|
@SubscriptionLevelID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT SubscriptionLevelID, Name, DisplayName, Description, MaxBooks, MaxChaptersPerBook,
|
||||||
|
MaxScenesPerBook, MaxCharactersPerProject, MaxStorageMB, MaxCollaborators, IsActive,
|
||||||
|
SortOrder, CreatedDateUTC, ModifiedDateUTC
|
||||||
|
FROM dbo.SubscriptionLevel
|
||||||
|
WHERE SubscriptionLevelID = @SubscriptionLevelID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.UserSubscription_GetCurrent
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
SELECT us.UserSubscriptionID, us.UserID, us.SubscriptionLevelID, us.StartDateUTC, us.EndDateUTC,
|
||||||
|
us.IsActive, us.IsTrial, us.TrialExpiryDateUTC, us.CreatedDateUTC, us.ModifiedDateUTC,
|
||||||
|
sl.Name, sl.DisplayName, sl.Description, sl.MaxBooks, sl.MaxChaptersPerBook,
|
||||||
|
sl.MaxScenesPerBook, sl.MaxCharactersPerProject, sl.MaxStorageMB, sl.MaxCollaborators,
|
||||||
|
sl.IsActive AS SubscriptionLevelIsActive, sl.SortOrder
|
||||||
|
FROM dbo.UserSubscription us
|
||||||
|
INNER JOIN dbo.SubscriptionLevel sl ON sl.SubscriptionLevelID = us.SubscriptionLevelID
|
||||||
|
WHERE us.UserID = @UserID
|
||||||
|
AND us.IsActive = 1
|
||||||
|
AND (us.EndDateUTC IS NULL OR us.EndDateUTC > SYSUTCDATETIME());
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.UserSubscription_AssignDefaultDraftDesk
|
||||||
|
@UserID int
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
DECLARE @DraftDeskID int;
|
||||||
|
SELECT @DraftDeskID = SubscriptionLevelID
|
||||||
|
FROM dbo.SubscriptionLevel
|
||||||
|
WHERE Name = N'DraftDesk';
|
||||||
|
|
||||||
|
IF @DraftDeskID IS NULL
|
||||||
|
RETURN;
|
||||||
|
|
||||||
|
IF NOT EXISTS
|
||||||
|
(
|
||||||
|
SELECT 1
|
||||||
|
FROM dbo.UserSubscription
|
||||||
|
WHERE UserID = @UserID
|
||||||
|
AND IsActive = 1
|
||||||
|
)
|
||||||
|
BEGIN
|
||||||
|
INSERT INTO dbo.UserSubscription (UserID, SubscriptionLevelID, StartDateUTC, IsActive, IsTrial)
|
||||||
|
VALUES (@UserID, @DraftDeskID, SYSUTCDATETIME(), 1, 0);
|
||||||
|
END;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE dbo.User_Create
|
||||||
|
@Email nvarchar(256),
|
||||||
|
@DisplayName nvarchar(200),
|
||||||
|
@PasswordHash nvarchar(max),
|
||||||
|
@EmailConfirmed bit = 0
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON;
|
||||||
|
|
||||||
|
INSERT INTO dbo.AppUser (Email, DisplayName, PasswordHash, EmailConfirmed)
|
||||||
|
VALUES (@Email, @DisplayName, @PasswordHash, @EmailConfirmed);
|
||||||
|
|
||||||
|
DECLARE @UserID int = SCOPE_IDENTITY();
|
||||||
|
EXEC dbo.UserSubscription_AssignDefaultDraftDesk @UserID = @UserID;
|
||||||
|
|
||||||
|
EXEC dbo.User_GetById @UserID = @UserID;
|
||||||
|
END;
|
||||||
|
GO
|
||||||
@ -1,7 +1,14 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using PlotLine.Models;
|
||||||
|
|
||||||
namespace PlotLine.ViewModels;
|
namespace PlotLine.ViewModels;
|
||||||
|
|
||||||
|
public sealed class ManageAccountViewModel
|
||||||
|
{
|
||||||
|
public AppUser User { get; set; } = new();
|
||||||
|
public UserSubscriptionDetail? Subscription { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class RegisterViewModel
|
public sealed class RegisterViewModel
|
||||||
{
|
{
|
||||||
[Required(ErrorMessage = "Enter your email address.")]
|
[Required(ErrorMessage = "Enter your email address.")]
|
||||||
|
|||||||
@ -1,13 +1,14 @@
|
|||||||
@model AppUser
|
@model ManageAccountViewModel
|
||||||
@{
|
@{
|
||||||
ViewData["Title"] = "Manage Account";
|
ViewData["Title"] = "Manage Account";
|
||||||
|
var subscription = Model.Subscription;
|
||||||
}
|
}
|
||||||
|
|
||||||
<div class="page-heading compact">
|
<div class="page-heading compact">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Account</p>
|
<p class="eyebrow">Account</p>
|
||||||
<h1>Manage Account</h1>
|
<h1>Manage Account</h1>
|
||||||
<p class="lead-text">@Model.Email</p>
|
<p class="lead-text">@Model.User.Email</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -21,15 +22,15 @@
|
|||||||
<div class="row g-3 mb-3">
|
<div class="row g-3 mb-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<p class="eyebrow">Email address</p>
|
<p class="eyebrow">Email address</p>
|
||||||
<p>@Model.Email</p>
|
<p>@Model.User.Email</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<p class="eyebrow">Email verified</p>
|
<p class="eyebrow">Email verified</p>
|
||||||
<p>@(Model.EmailConfirmed ? "Yes" : "No")</p>
|
<p>@(Model.User.EmailConfirmed ? "Yes" : "No")</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<p class="eyebrow">Two-factor authentication</p>
|
<p class="eyebrow">Two-factor authentication</p>
|
||||||
<p>@(Model.TwoFactorEnabled ? "Enabled" : "Not enabled")</p>
|
<p>@(Model.User.TwoFactorEnabled ? "Enabled" : "Not enabled")</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row mb-4">
|
<div class="button-row mb-4">
|
||||||
@ -39,7 +40,7 @@
|
|||||||
<h2>Two-Factor Authentication</h2>
|
<h2>Two-Factor Authentication</h2>
|
||||||
<p class="muted">Use an authenticator app to add an extra sign-in step for your account.</p>
|
<p class="muted">Use an authenticator app to add an extra sign-in step for your account.</p>
|
||||||
<div class="button-row">
|
<div class="button-row">
|
||||||
@if (Model.TwoFactorEnabled)
|
@if (Model.User.TwoFactorEnabled)
|
||||||
{
|
{
|
||||||
<a class="btn btn-outline-primary" asp-action="RecoveryCodes">Recovery Codes</a>
|
<a class="btn btn-outline-primary" asp-action="RecoveryCodes">Recovery Codes</a>
|
||||||
<a class="btn btn-outline-danger" asp-action="DisableTwoFactor">Disable Two Factor</a>
|
<a class="btn btn-outline-danger" asp-action="DisableTwoFactor">Disable Two Factor</a>
|
||||||
@ -50,3 +51,50 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="edit-panel">
|
||||||
|
<h2>Subscription</h2>
|
||||||
|
@if (subscription is null)
|
||||||
|
{
|
||||||
|
<p class="muted">No active subscription is assigned.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Current plan</p>
|
||||||
|
<p>@subscription.DisplayName</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Books</p>
|
||||||
|
<p>@FormatLimit(subscription.MaxBooks)</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Storage</p>
|
||||||
|
<p>@FormatStorage(subscription.MaxStorageMB)</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Chapters per book</p>
|
||||||
|
<p>@FormatLimit(subscription.MaxChaptersPerBook)</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Scenes per book</p>
|
||||||
|
<p>@FormatLimit(subscription.MaxScenesPerBook)</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Characters per project</p>
|
||||||
|
<p>@FormatLimit(subscription.MaxCharactersPerProject)</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<p class="eyebrow">Collaborators</p>
|
||||||
|
<p>@subscription.MaxCollaborators</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@functions {
|
||||||
|
private static string FormatLimit(int value) => value >= 999999 ? "Practical unlimited" : value.ToString("N0");
|
||||||
|
|
||||||
|
private static string FormatStorage(int value) => value >= 1024 ? $"{value / 1024:N0} GB" : $"{value:N0} MB";
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user