Phase 4A AuthService
This commit is contained in:
parent
397035981c
commit
6f42c65e35
@ -0,0 +1,18 @@
|
||||
{
|
||||
"Microsoft.NET.Workload.Emscripten.Current": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Workload.Emscripten.net6": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Workload.Emscripten.net7": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Workload.Emscripten.net8": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Workload.Emscripten.net9": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Sdk.Android": "36.1.53/10.0.100",
|
||||
"Microsoft.NET.Sdk.iOS": "26.5.10284/10.0.100",
|
||||
"Microsoft.NET.Sdk.MacCatalyst": "26.5.10284/10.0.100",
|
||||
"Microsoft.NET.Sdk.macOS": "26.5.10284/10.0.100",
|
||||
"Microsoft.NET.Sdk.Maui": "10.0.20/10.0.100",
|
||||
"Microsoft.NET.Sdk.tvOS": "26.5.10284/10.0.100",
|
||||
"Microsoft.NET.Workload.Mono.ToolChain.Current": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Workload.Mono.ToolChain.net6": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Workload.Mono.ToolChain.net7": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Workload.Mono.ToolChain.net8": "10.0.108/10.0.100",
|
||||
"Microsoft.NET.Workload.Mono.ToolChain.net9": "10.0.108/10.0.100"
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
10.0.300
|
||||
@ -0,0 +1 @@
|
||||
10.0.300.3
|
||||
6
.appdata/runner-appdata/NuGet/NuGet.Config
Normal file
6
.appdata/runner-appdata/NuGet/NuGet.Config
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
</packageSources>
|
||||
</configuration>
|
||||
@ -0,0 +1,35 @@
|
||||
{
|
||||
"allRepositorySigned": true,
|
||||
"signingCertificates": [
|
||||
{
|
||||
"fingerprints": {
|
||||
"2.16.840.1.101.3.4.2.1": "0e5f38f57dc1bcc806d8494f4f90fbcedd988b46760709cbeec6f4219aa6157d"
|
||||
},
|
||||
"subject": "CN=NuGet.org Repository by Microsoft, O=NuGet.org Repository by Microsoft, L=Redmond, S=Washington, C=US",
|
||||
"issuer": "CN=DigiCert SHA2 Assured ID Code Signing CA, OU=www.digicert.com, O=DigiCert Inc, C=US",
|
||||
"notBefore": "2018-04-10T00:00:00.0000000Z",
|
||||
"notAfter": "2021-04-14T12:00:00.0000000Z",
|
||||
"contentUrl": "https://api.nuget.org/v3-index/repository-signatures/certificates/0e5f38f57dc1bcc806d8494f4f90fbcedd988b46760709cbeec6f4219aa6157d.crt"
|
||||
},
|
||||
{
|
||||
"fingerprints": {
|
||||
"2.16.840.1.101.3.4.2.1": "5a2901d6ada3d18260b9c6dfe2133c95d74b9eef6ae0e5dc334c8454d1477df4"
|
||||
},
|
||||
"subject": "CN=NuGet.org Repository by Microsoft, O=NuGet.org Repository by Microsoft, L=Redmond, S=Washington, C=US",
|
||||
"issuer": "CN=DigiCert SHA2 Assured ID Code Signing CA, OU=www.digicert.com, O=DigiCert Inc, C=US",
|
||||
"notBefore": "2021-02-16T00:00:00.0000000Z",
|
||||
"notAfter": "2024-05-15T23:59:59.0000000Z",
|
||||
"contentUrl": "https://api.nuget.org/v3-index/repository-signatures/certificates/5a2901d6ada3d18260b9c6dfe2133c95d74b9eef6ae0e5dc334c8454d1477df4.crt"
|
||||
},
|
||||
{
|
||||
"fingerprints": {
|
||||
"2.16.840.1.101.3.4.2.1": "1f4b311d9acc115c8dc8018b5a49e00fce6da8e2855f9f014ca6f34570bc482d"
|
||||
},
|
||||
"subject": "CN=NuGet.org Repository by Microsoft, O=NuGet.org Repository by Microsoft, L=Redmond, S=Washington, C=US",
|
||||
"issuer": "CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1, O=\"DigiCert, Inc.\", C=US",
|
||||
"notBefore": "2024-02-23T00:00:00.0000000Z",
|
||||
"notAfter": "2027-05-18T23:59:59.0000000Z",
|
||||
"contentUrl": "https://api.nuget.org/v3-index/repository-signatures/certificates/1f4b311d9acc115c8dc8018b5a49e00fce6da8e2855f9f014ca6f34570bc482d.crt"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,207 @@
|
||||
{
|
||||
"version": "3.0.0",
|
||||
"resources": [
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/query",
|
||||
"@type": "SearchQueryService",
|
||||
"comment": "Query endpoint of NuGet Search service (primary)"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/query",
|
||||
"@type": "SearchQueryService",
|
||||
"comment": "Query endpoint of NuGet Search service (secondary)"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/autocomplete",
|
||||
"@type": "SearchAutocompleteService",
|
||||
"comment": "Autocomplete endpoint of NuGet Search service (primary)"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/autocomplete",
|
||||
"@type": "SearchAutocompleteService",
|
||||
"comment": "Autocomplete endpoint of NuGet Search service (secondary)"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/",
|
||||
"@type": "SearchGalleryQueryService/3.0.0-rc",
|
||||
"comment": "Azure Website based Search Service used by Gallery (primary)"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/",
|
||||
"@type": "SearchGalleryQueryService/3.0.0-rc",
|
||||
"comment": "Azure Website based Search Service used by Gallery (secondary)"
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/registration5-semver1/",
|
||||
"@type": "RegistrationsBaseUrl",
|
||||
"comment": "Base URL of Azure storage where NuGet package registration info is stored"
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3-flatcontainer/",
|
||||
"@type": "PackageBaseAddress/3.0.0",
|
||||
"comment": "Base URL of where NuGet packages are stored, in the format https://api.nuget.org/v3-flatcontainer/{id-lower}/{version-lower}/{id-lower}.{version-lower}.nupkg"
|
||||
},
|
||||
{
|
||||
"@id": "https://www.nuget.org/api/v2",
|
||||
"@type": "LegacyGallery"
|
||||
},
|
||||
{
|
||||
"@id": "https://www.nuget.org/api/v2",
|
||||
"@type": "LegacyGallery/2.0.0"
|
||||
},
|
||||
{
|
||||
"@id": "https://www.nuget.org/api/v2/package",
|
||||
"@type": "PackagePublish/2.0.0"
|
||||
},
|
||||
{
|
||||
"@id": "https://www.nuget.org/api/v2/symbolpackage",
|
||||
"@type": "SymbolPackagePublish/4.9.0",
|
||||
"comment": "The gallery symbol publish endpoint."
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/query",
|
||||
"@type": "SearchQueryService/3.0.0-rc",
|
||||
"comment": "Query endpoint of NuGet Search service (primary) used by RC clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/query",
|
||||
"@type": "SearchQueryService/3.0.0-rc",
|
||||
"comment": "Query endpoint of NuGet Search service (secondary) used by RC clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/query",
|
||||
"@type": "SearchQueryService/3.5.0",
|
||||
"comment": "Query endpoint of NuGet Search service (primary) that supports package type filtering"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/query",
|
||||
"@type": "SearchQueryService/3.5.0",
|
||||
"comment": "Query endpoint of NuGet Search service (secondary) that supports package type filtering"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/autocomplete",
|
||||
"@type": "SearchAutocompleteService/3.0.0-rc",
|
||||
"comment": "Autocomplete endpoint of NuGet Search service (primary) used by RC clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/autocomplete",
|
||||
"@type": "SearchAutocompleteService/3.0.0-rc",
|
||||
"comment": "Autocomplete endpoint of NuGet Search service (secondary) used by RC clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/autocomplete",
|
||||
"@type": "SearchAutocompleteService/3.5.0",
|
||||
"comment": "Autocomplete endpoint of NuGet Search service (primary) that supports package type filtering"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/autocomplete",
|
||||
"@type": "SearchAutocompleteService/3.5.0",
|
||||
"comment": "Autocomplete endpoint of NuGet Search service (secondary) that supports package type filtering"
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/registration5-semver1/",
|
||||
"@type": "RegistrationsBaseUrl/3.0.0-rc",
|
||||
"comment": "Base URL of Azure storage where NuGet package registration info is stored used by RC clients. This base URL does not include SemVer 2.0.0 packages."
|
||||
},
|
||||
{
|
||||
"@id": "https://www.nuget.org/packages/{id}/{version}/ReportAbuse",
|
||||
"@type": "ReportAbuseUriTemplate/3.0.0-rc",
|
||||
"comment": "URI template used by NuGet Client to construct Report Abuse URL for packages used by RC clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/registration5-semver1/{id-lower}/index.json",
|
||||
"@type": "PackageDisplayMetadataUriTemplate/3.0.0-rc",
|
||||
"comment": "URI template used by NuGet Client to construct display metadata for Packages using ID"
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/registration5-semver1/{id-lower}/{version-lower}.json",
|
||||
"@type": "PackageVersionDisplayMetadataUriTemplate/3.0.0-rc",
|
||||
"comment": "URI template used by NuGet Client to construct display metadata for Packages using ID, Version"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/query",
|
||||
"@type": "SearchQueryService/3.0.0-beta",
|
||||
"comment": "Query endpoint of NuGet Search service (primary) used by beta clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/query",
|
||||
"@type": "SearchQueryService/3.0.0-beta",
|
||||
"comment": "Query endpoint of NuGet Search service (secondary) used by beta clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-usnc.nuget.org/autocomplete",
|
||||
"@type": "SearchAutocompleteService/3.0.0-beta",
|
||||
"comment": "Autocomplete endpoint of NuGet Search service (primary) used by beta clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://azuresearch-ussc.nuget.org/autocomplete",
|
||||
"@type": "SearchAutocompleteService/3.0.0-beta",
|
||||
"comment": "Autocomplete endpoint of NuGet Search service (secondary) used by beta clients"
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/registration5-semver1/",
|
||||
"@type": "RegistrationsBaseUrl/3.0.0-beta",
|
||||
"comment": "Base URL of Azure storage where NuGet package registration info is stored used by Beta clients. This base URL does not include SemVer 2.0.0 packages."
|
||||
},
|
||||
{
|
||||
"@id": "https://www.nuget.org/packages/{id}/{version}/ReportAbuse",
|
||||
"@type": "ReportAbuseUriTemplate/3.0.0-beta",
|
||||
"comment": "URI template used by NuGet Client to construct Report Abuse URL for packages"
|
||||
},
|
||||
{
|
||||
"@id": "https://www.nuget.org/packages/{id}/{version}?_src=template",
|
||||
"@type": "PackageDetailsUriTemplate/5.1.0",
|
||||
"comment": "URI template used by NuGet Client to construct details URL for packages"
|
||||
},
|
||||
{
|
||||
"@id": "https://www.nuget.org/profiles/{owner}?_src=template",
|
||||
"@type": "OwnerDetailsUriTemplate/6.11.0",
|
||||
"comment": "URI template used by NuGet Client to construct owner URL for packages"
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/registration5-gz-semver1/",
|
||||
"@type": "RegistrationsBaseUrl/3.4.0",
|
||||
"comment": "Base URL of Azure storage where NuGet package registration info is stored in GZIP format. This base URL does not include SemVer 2.0.0 packages."
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/registration5-gz-semver2/",
|
||||
"@type": "RegistrationsBaseUrl/3.6.0",
|
||||
"comment": "Base URL of Azure storage where NuGet package registration info is stored in GZIP format. This base URL includes SemVer 2.0.0 packages."
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/registration5-gz-semver2/",
|
||||
"@type": "RegistrationsBaseUrl/Versioned",
|
||||
"clientVersion": "4.3.0-alpha",
|
||||
"comment": "Base URL of Azure storage where NuGet package registration info is stored in GZIP format. This base URL includes SemVer 2.0.0 packages."
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3-index/repository-signatures/4.7.0/index.json",
|
||||
"@type": "RepositorySignatures/4.7.0",
|
||||
"comment": "The endpoint for discovering information about this package source's repository signatures."
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3-index/repository-signatures/5.0.0/index.json",
|
||||
"@type": "RepositorySignatures/5.0.0",
|
||||
"comment": "The endpoint for discovering information about this package source's repository signatures."
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/vulnerabilities/index.json",
|
||||
"@type": "VulnerabilityInfo/6.7.0",
|
||||
"comment": "The endpoint for discovering information about vulnerabilities of packages in this package source."
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3/catalog0/index.json",
|
||||
"@type": "Catalog/3.0.0",
|
||||
"comment": "Index of the NuGet package catalog."
|
||||
},
|
||||
{
|
||||
"@id": "https://api.nuget.org/v3-flatcontainer/{lower_id}/{lower_version}/readme",
|
||||
"@type": "ReadmeUriTemplate/6.13.0",
|
||||
"comment": "URI template used by NuGet Client to construct a URL for downloading a package's README."
|
||||
}
|
||||
],
|
||||
"@context": {
|
||||
"@vocab": "http://schema.nuget.org/services#",
|
||||
"comment": "http://www.w3.org/2000/01/rdf-schema#comment"
|
||||
}
|
||||
}
|
||||
187
PlotLine/Data/AuthRepositories.cs
Normal file
187
PlotLine/Data/AuthRepositories.cs
Normal file
@ -0,0 +1,187 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Data;
|
||||
|
||||
public interface IUserRepository
|
||||
{
|
||||
Task<AppUser?> GetByEmailAsync(string email);
|
||||
Task<AppUser?> GetByIdAsync(int userId);
|
||||
Task<AppUser> CreateAsync(string email, string displayName, string passwordHash);
|
||||
Task<AppUser?> UpdateLoginSuccessAsync(int userId);
|
||||
Task<AppUser?> UpdateFailedLoginAsync(int userId, DateTime? lockoutEndUtc);
|
||||
Task<bool> ConfirmEmailAsync(int userId, Guid token);
|
||||
Task<bool> ResetPasswordAsync(int userId, Guid token, string passwordHash);
|
||||
Task<AppUser?> EnableTwoFactorAsync(int userId);
|
||||
Task<AppUser?> DisableTwoFactorAsync(int userId);
|
||||
}
|
||||
|
||||
public interface IAuthenticationRepository
|
||||
{
|
||||
Task<UserEmailVerificationToken> CreateEmailVerificationTokenAsync(int userId, Guid token, DateTime expiryUtc);
|
||||
Task<UserEmailVerificationToken?> GetEmailVerificationTokenAsync(Guid token);
|
||||
Task<UserPasswordResetToken> CreatePasswordResetTokenAsync(int userId, Guid token, DateTime expiryUtc);
|
||||
Task<UserPasswordResetToken?> GetPasswordResetTokenAsync(Guid token);
|
||||
Task<UserTwoFactor?> GetTwoFactorAsync(int userId);
|
||||
Task<UserTwoFactor> SaveTwoFactorAsync(int userId, string secretKey, string? recoveryCodes);
|
||||
Task<long> InsertLoginAuditAsync(UserLoginAudit audit);
|
||||
}
|
||||
|
||||
public sealed class UserRepository(ISqlConnectionFactory connectionFactory) : IUserRepository
|
||||
{
|
||||
public async Task<AppUser?> GetByEmailAsync(string email)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<AppUser>(
|
||||
"dbo.User_GetByEmail",
|
||||
new { Email = email },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<AppUser?> GetByIdAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<AppUser>(
|
||||
"dbo.User_GetById",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<AppUser> CreateAsync(string email, string displayName, string passwordHash)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<AppUser>(
|
||||
"dbo.User_Create",
|
||||
new { Email = email, DisplayName = displayName, PasswordHash = passwordHash },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<AppUser?> UpdateLoginSuccessAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<AppUser>(
|
||||
"dbo.User_UpdateLoginSuccess",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<AppUser?> UpdateFailedLoginAsync(int userId, DateTime? lockoutEndUtc)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<AppUser>(
|
||||
"dbo.User_UpdateFailedLogin",
|
||||
new { UserID = userId, LockoutEndUtc = lockoutEndUtc },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> ConfirmEmailAsync(int userId, Guid token)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<bool>(
|
||||
"dbo.User_ConfirmEmail",
|
||||
new { UserID = userId, Token = token },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<bool> ResetPasswordAsync(int userId, Guid token, string passwordHash)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<bool>(
|
||||
"dbo.User_ResetPassword",
|
||||
new { UserID = userId, Token = token, PasswordHash = passwordHash },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<AppUser?> EnableTwoFactorAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<AppUser>(
|
||||
"dbo.User_EnableTwoFactor",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<AppUser?> DisableTwoFactorAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<AppUser>(
|
||||
"dbo.User_DisableTwoFactor",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AuthenticationRepository(ISqlConnectionFactory connectionFactory) : IAuthenticationRepository
|
||||
{
|
||||
public async Task<UserEmailVerificationToken> CreateEmailVerificationTokenAsync(int userId, Guid token, DateTime expiryUtc)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<UserEmailVerificationToken>(
|
||||
"dbo.User_CreateEmailVerificationToken",
|
||||
new { UserID = userId, Token = token, ExpiryUtc = expiryUtc },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<UserEmailVerificationToken?> GetEmailVerificationTokenAsync(Guid token)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<UserEmailVerificationToken>(
|
||||
"dbo.User_GetEmailVerificationToken",
|
||||
new { Token = token },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<UserPasswordResetToken> CreatePasswordResetTokenAsync(int userId, Guid token, DateTime expiryUtc)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<UserPasswordResetToken>(
|
||||
"dbo.User_CreatePasswordResetToken",
|
||||
new { UserID = userId, Token = token, ExpiryUtc = expiryUtc },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<UserPasswordResetToken?> GetPasswordResetTokenAsync(Guid token)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<UserPasswordResetToken>(
|
||||
"dbo.User_GetPasswordResetToken",
|
||||
new { Token = token },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<UserTwoFactor?> GetTwoFactorAsync(int userId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<UserTwoFactor>(
|
||||
"dbo.User_GetTwoFactor",
|
||||
new { UserID = userId },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<UserTwoFactor> SaveTwoFactorAsync(int userId, string secretKey, string? recoveryCodes)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<UserTwoFactor>(
|
||||
"dbo.User_SaveTwoFactor",
|
||||
new { UserID = userId, SecretKey = secretKey, RecoveryCodes = recoveryCodes },
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
|
||||
public async Task<long> InsertLoginAuditAsync(UserLoginAudit audit)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleAsync<long>(
|
||||
"dbo.User_InsertLoginAudit",
|
||||
new
|
||||
{
|
||||
audit.UserID,
|
||||
audit.EmailAttempted,
|
||||
audit.IpAddress,
|
||||
audit.UserAgent,
|
||||
audit.WasSuccessful,
|
||||
audit.Reason
|
||||
},
|
||||
commandType: CommandType.StoredProcedure);
|
||||
}
|
||||
}
|
||||
55
PlotLine/Models/AuthModels.cs
Normal file
55
PlotLine/Models/AuthModels.cs
Normal file
@ -0,0 +1,55 @@
|
||||
namespace PlotLine.Models;
|
||||
|
||||
public sealed class AppUser
|
||||
{
|
||||
public int UserID { get; set; }
|
||||
public string Email { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string PasswordHash { get; set; } = string.Empty;
|
||||
public bool EmailConfirmed { get; set; }
|
||||
public bool IsLocked { get; set; }
|
||||
public int FailedLoginAttempts { get; set; }
|
||||
public DateTime? LockoutEndUtc { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
public DateTime UpdatedUtc { get; set; }
|
||||
public DateTime? LastLoginUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UserEmailVerificationToken
|
||||
{
|
||||
public int TokenID { get; set; }
|
||||
public int UserID { get; set; }
|
||||
public Guid Token { get; set; }
|
||||
public DateTime ExpiryUtc { get; set; }
|
||||
public DateTime? UsedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UserPasswordResetToken
|
||||
{
|
||||
public int TokenID { get; set; }
|
||||
public int UserID { get; set; }
|
||||
public Guid Token { get; set; }
|
||||
public DateTime ExpiryUtc { get; set; }
|
||||
public DateTime? UsedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UserTwoFactor
|
||||
{
|
||||
public int UserID { get; set; }
|
||||
public string SecretKey { get; set; } = string.Empty;
|
||||
public string? RecoveryCodes { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
}
|
||||
|
||||
public sealed class UserLoginAudit
|
||||
{
|
||||
public long AuditID { get; set; }
|
||||
public int? UserID { get; set; }
|
||||
public string? EmailAttempted { get; set; }
|
||||
public string? IpAddress { get; set; }
|
||||
public string? UserAgent { get; set; }
|
||||
public bool WasSuccessful { get; set; }
|
||||
public string? Reason { get; set; }
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Services;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
|
||||
namespace PlotLine;
|
||||
@ -17,7 +18,23 @@ public class Program
|
||||
builder.Services.AddControllersWithViews();
|
||||
builder.Services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(builder.Environment.ContentRootPath, "App_Data", "DataProtectionKeys")));
|
||||
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.AddCookie(options =>
|
||||
{
|
||||
options.Cookie.Name = "PlotWeaver.Auth";
|
||||
options.LoginPath = "/Account/Login";
|
||||
options.LogoutPath = "/Account/Logout";
|
||||
options.AccessDeniedPath = "/Account/AccessDenied";
|
||||
options.SlidingExpiration = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(14);
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||
});
|
||||
builder.Services.AddAuthorization();
|
||||
builder.Services.AddSingleton<ISqlConnectionFactory, SqlConnectionFactory>();
|
||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||
builder.Services.AddScoped<IAuthenticationRepository, AuthenticationRepository>();
|
||||
builder.Services.AddScoped<IProjectRepository, ProjectRepository>();
|
||||
builder.Services.AddScoped<IBookRepository, BookRepository>();
|
||||
builder.Services.AddScoped<IChapterRepository, ChapterRepository>();
|
||||
@ -70,6 +87,11 @@ public class Program
|
||||
builder.Services.AddScoped<IImportService, ImportService>();
|
||||
builder.Services.AddSingleton<IHelpService, HelpService>();
|
||||
builder.Services.AddSingleton<IHelpMarkdownRenderer, HelpMarkdownRenderer>();
|
||||
builder.Services.AddScoped<IAuthService>(_ => throw new NotImplementedException("Authentication business logic will be implemented in a later phase."));
|
||||
builder.Services.AddScoped<IPasswordService>(_ => throw new NotImplementedException("Password hashing will be implemented in a later phase."));
|
||||
builder.Services.AddScoped<ITwoFactorService>(_ => throw new NotImplementedException("Two-factor authentication will be implemented in a later phase."));
|
||||
builder.Services.AddScoped<IEmailService>(_ => throw new NotImplementedException("Authentication email sending will be implemented in a later phase."));
|
||||
builder.Services.AddScoped<ICurrentUserService>(_ => throw new NotImplementedException("Current user access will be implemented in a later phase."));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@ -83,6 +105,7 @@ public class Program
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
|
||||
app.MapStaticAssets();
|
||||
|
||||
46
PlotLine/Services/AuthServiceInterfaces.cs
Normal file
46
PlotLine/Services/AuthServiceInterfaces.cs
Normal file
@ -0,0 +1,46 @@
|
||||
using PlotLine.ViewModels;
|
||||
|
||||
namespace PlotLine.Services;
|
||||
|
||||
public interface IAuthService
|
||||
{
|
||||
Task<bool> RegisterAsync(RegisterViewModel model);
|
||||
Task<bool> LoginAsync(LoginViewModel model);
|
||||
Task<bool> CompleteTwoFactorLoginAsync(TwoFactorViewModel model);
|
||||
Task LogoutAsync();
|
||||
Task<bool> VerifyEmailAsync(Guid token);
|
||||
Task<bool> ForgotPasswordAsync(ForgotPasswordViewModel model);
|
||||
Task<bool> ResetPasswordAsync(ResetPasswordViewModel model);
|
||||
Task<bool> ChangePasswordAsync(ChangePasswordViewModel model);
|
||||
Task<bool> EnableTwoFactorAsync(int userId);
|
||||
Task<bool> DisableTwoFactorAsync(int userId);
|
||||
Task<IReadOnlyList<string>> GenerateRecoveryCodesAsync(int userId);
|
||||
}
|
||||
|
||||
public interface IPasswordService
|
||||
{
|
||||
string HashPassword(string password);
|
||||
bool VerifyPassword(string password, string passwordHash);
|
||||
}
|
||||
|
||||
public interface ITwoFactorService
|
||||
{
|
||||
string GenerateSecretKey();
|
||||
string GenerateQrCodeUri(string email, string secretKey);
|
||||
bool ValidateCode(string secretKey, string code);
|
||||
IReadOnlyList<string> GenerateRecoveryCodes(int count);
|
||||
}
|
||||
|
||||
public interface IEmailService
|
||||
{
|
||||
Task SendVerificationEmailAsync(string email, string displayName, Guid token);
|
||||
Task SendPasswordResetEmailAsync(string email, string displayName, Guid token);
|
||||
}
|
||||
|
||||
public interface ICurrentUserService
|
||||
{
|
||||
int? UserId { get; }
|
||||
string? Email { get; }
|
||||
string? DisplayName { get; }
|
||||
bool IsAuthenticated { get; }
|
||||
}
|
||||
395
PlotLine/Sql/037_Phase4A_BespokeAuthFoundation.sql
Normal file
395
PlotLine/Sql/037_Phase4A_BespokeAuthFoundation.sql
Normal file
@ -0,0 +1,395 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.AppUser', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.AppUser
|
||||
(
|
||||
UserID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_AppUser PRIMARY KEY,
|
||||
Email nvarchar(256) NOT NULL,
|
||||
DisplayName nvarchar(200) NOT NULL,
|
||||
PasswordHash nvarchar(max) NOT NULL,
|
||||
EmailConfirmed bit NOT NULL CONSTRAINT DF_AppUser_EmailConfirmed DEFAULT 0,
|
||||
IsLocked bit NOT NULL CONSTRAINT DF_AppUser_IsLocked DEFAULT 0,
|
||||
FailedLoginAttempts int NOT NULL CONSTRAINT DF_AppUser_FailedLoginAttempts DEFAULT 0,
|
||||
LockoutEndUtc datetime2 NULL,
|
||||
TwoFactorEnabled bit NOT NULL CONSTRAINT DF_AppUser_TwoFactorEnabled DEFAULT 0,
|
||||
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_AppUser_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedUtc datetime2 NOT NULL CONSTRAINT DF_AppUser_UpdatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
LastLoginUtc datetime2 NULL
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_AppUser_Email' AND object_id = OBJECT_ID(N'dbo.AppUser'))
|
||||
CREATE UNIQUE INDEX UX_AppUser_Email ON dbo.AppUser(Email);
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.UserEmailVerificationToken', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.UserEmailVerificationToken
|
||||
(
|
||||
TokenID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_UserEmailVerificationToken PRIMARY KEY,
|
||||
UserID int NOT NULL,
|
||||
Token uniqueidentifier NOT NULL,
|
||||
ExpiryUtc datetime2 NOT NULL,
|
||||
UsedUtc datetime2 NULL,
|
||||
CONSTRAINT FK_UserEmailVerificationToken_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_UserEmailVerificationToken_Token' AND object_id = OBJECT_ID(N'dbo.UserEmailVerificationToken'))
|
||||
CREATE UNIQUE INDEX UX_UserEmailVerificationToken_Token ON dbo.UserEmailVerificationToken(Token);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserEmailVerificationToken_UserID' AND object_id = OBJECT_ID(N'dbo.UserEmailVerificationToken'))
|
||||
CREATE INDEX IX_UserEmailVerificationToken_UserID ON dbo.UserEmailVerificationToken(UserID, ExpiryUtc DESC);
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.UserPasswordResetToken', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.UserPasswordResetToken
|
||||
(
|
||||
TokenID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_UserPasswordResetToken PRIMARY KEY,
|
||||
UserID int NOT NULL,
|
||||
Token uniqueidentifier NOT NULL,
|
||||
ExpiryUtc datetime2 NOT NULL,
|
||||
UsedUtc datetime2 NULL,
|
||||
CONSTRAINT FK_UserPasswordResetToken_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_UserPasswordResetToken_Token' AND object_id = OBJECT_ID(N'dbo.UserPasswordResetToken'))
|
||||
CREATE UNIQUE INDEX UX_UserPasswordResetToken_Token ON dbo.UserPasswordResetToken(Token);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserPasswordResetToken_UserID' AND object_id = OBJECT_ID(N'dbo.UserPasswordResetToken'))
|
||||
CREATE INDEX IX_UserPasswordResetToken_UserID ON dbo.UserPasswordResetToken(UserID, ExpiryUtc DESC);
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.UserTwoFactor', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.UserTwoFactor
|
||||
(
|
||||
UserID int NOT NULL CONSTRAINT PK_UserTwoFactor PRIMARY KEY,
|
||||
SecretKey nvarchar(500) NOT NULL,
|
||||
RecoveryCodes nvarchar(max) NULL,
|
||||
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_UserTwoFactor_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_UserTwoFactor_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.UserLoginAudit', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.UserLoginAudit
|
||||
(
|
||||
AuditID bigint IDENTITY(1,1) NOT NULL CONSTRAINT PK_UserLoginAudit PRIMARY KEY,
|
||||
UserID int NULL,
|
||||
EmailAttempted nvarchar(256) NULL,
|
||||
IpAddress nvarchar(100) NULL,
|
||||
UserAgent nvarchar(500) NULL,
|
||||
WasSuccessful bit NOT NULL,
|
||||
Reason nvarchar(200) NULL,
|
||||
CreatedUtc datetime2 NOT NULL CONSTRAINT DF_UserLoginAudit_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT FK_UserLoginAudit_AppUser FOREIGN KEY (UserID) REFERENCES dbo.AppUser(UserID)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserLoginAudit_UserCreated' AND object_id = OBJECT_ID(N'dbo.UserLoginAudit'))
|
||||
CREATE INDEX IX_UserLoginAudit_UserCreated ON dbo.UserLoginAudit(UserID, CreatedUtc DESC);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_UserLoginAudit_EmailCreated' AND object_id = OBJECT_ID(N'dbo.UserLoginAudit'))
|
||||
CREATE INDEX IX_UserLoginAudit_EmailCreated ON dbo.UserLoginAudit(EmailAttempted, CreatedUtc DESC);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_GetByEmail
|
||||
@Email nvarchar(256)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserID, Email, DisplayName, PasswordHash, EmailConfirmed, IsLocked, FailedLoginAttempts,
|
||||
LockoutEndUtc, TwoFactorEnabled, CreatedUtc, UpdatedUtc, LastLoginUtc
|
||||
FROM dbo.AppUser
|
||||
WHERE Email = @Email;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_GetById
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserID, Email, DisplayName, PasswordHash, EmailConfirmed, IsLocked, FailedLoginAttempts,
|
||||
LockoutEndUtc, TwoFactorEnabled, CreatedUtc, UpdatedUtc, LastLoginUtc
|
||||
FROM dbo.AppUser
|
||||
WHERE UserID = @UserID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_Create
|
||||
@Email nvarchar(256),
|
||||
@DisplayName nvarchar(200),
|
||||
@PasswordHash nvarchar(max)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT INTO dbo.AppUser (Email, DisplayName, PasswordHash)
|
||||
VALUES (@Email, @DisplayName, @PasswordHash);
|
||||
|
||||
DECLARE @UserID int = SCOPE_IDENTITY();
|
||||
|
||||
EXEC dbo.User_GetById @UserID = @UserID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_UpdateLoginSuccess
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.AppUser
|
||||
SET FailedLoginAttempts = 0,
|
||||
IsLocked = 0,
|
||||
LockoutEndUtc = NULL,
|
||||
LastLoginUtc = SYSUTCDATETIME(),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE UserID = @UserID;
|
||||
|
||||
EXEC dbo.User_GetById @UserID = @UserID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_UpdateFailedLogin
|
||||
@UserID int,
|
||||
@LockoutEndUtc datetime2 = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.AppUser
|
||||
SET FailedLoginAttempts = FailedLoginAttempts + 1,
|
||||
IsLocked = CASE WHEN @LockoutEndUtc IS NULL THEN IsLocked ELSE 1 END,
|
||||
LockoutEndUtc = COALESCE(@LockoutEndUtc, LockoutEndUtc),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE UserID = @UserID;
|
||||
|
||||
EXEC dbo.User_GetById @UserID = @UserID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_CreateEmailVerificationToken
|
||||
@UserID int,
|
||||
@Token uniqueidentifier,
|
||||
@ExpiryUtc datetime2
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT INTO dbo.UserEmailVerificationToken (UserID, Token, ExpiryUtc)
|
||||
VALUES (@UserID, @Token, @ExpiryUtc);
|
||||
|
||||
DECLARE @TokenID int = SCOPE_IDENTITY();
|
||||
|
||||
SELECT TokenID, UserID, Token, ExpiryUtc, UsedUtc
|
||||
FROM dbo.UserEmailVerificationToken
|
||||
WHERE TokenID = @TokenID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_GetEmailVerificationToken
|
||||
@Token uniqueidentifier
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT TokenID, UserID, Token, ExpiryUtc, UsedUtc
|
||||
FROM dbo.UserEmailVerificationToken
|
||||
WHERE Token = @Token;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_ConfirmEmail
|
||||
@UserID int,
|
||||
@Token uniqueidentifier
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.UserEmailVerificationToken
|
||||
SET UsedUtc = SYSUTCDATETIME()
|
||||
WHERE UserID = @UserID
|
||||
AND Token = @Token
|
||||
AND UsedUtc IS NULL
|
||||
AND ExpiryUtc >= SYSUTCDATETIME();
|
||||
|
||||
IF @@ROWCOUNT = 1
|
||||
BEGIN
|
||||
UPDATE dbo.AppUser
|
||||
SET EmailConfirmed = 1,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE UserID = @UserID;
|
||||
|
||||
SELECT CAST(1 AS bit);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
SELECT CAST(0 AS bit);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_CreatePasswordResetToken
|
||||
@UserID int,
|
||||
@Token uniqueidentifier,
|
||||
@ExpiryUtc datetime2
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT INTO dbo.UserPasswordResetToken (UserID, Token, ExpiryUtc)
|
||||
VALUES (@UserID, @Token, @ExpiryUtc);
|
||||
|
||||
DECLARE @TokenID int = SCOPE_IDENTITY();
|
||||
|
||||
SELECT TokenID, UserID, Token, ExpiryUtc, UsedUtc
|
||||
FROM dbo.UserPasswordResetToken
|
||||
WHERE TokenID = @TokenID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_GetPasswordResetToken
|
||||
@Token uniqueidentifier
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT TokenID, UserID, Token, ExpiryUtc, UsedUtc
|
||||
FROM dbo.UserPasswordResetToken
|
||||
WHERE Token = @Token;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_ResetPassword
|
||||
@UserID int,
|
||||
@Token uniqueidentifier,
|
||||
@PasswordHash nvarchar(max)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.UserPasswordResetToken
|
||||
SET UsedUtc = SYSUTCDATETIME()
|
||||
WHERE UserID = @UserID
|
||||
AND Token = @Token
|
||||
AND UsedUtc IS NULL
|
||||
AND ExpiryUtc >= SYSUTCDATETIME();
|
||||
|
||||
IF @@ROWCOUNT = 1
|
||||
BEGIN
|
||||
UPDATE dbo.AppUser
|
||||
SET PasswordHash = @PasswordHash,
|
||||
FailedLoginAttempts = 0,
|
||||
IsLocked = 0,
|
||||
LockoutEndUtc = NULL,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE UserID = @UserID;
|
||||
|
||||
SELECT CAST(1 AS bit);
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
SELECT CAST(0 AS bit);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_EnableTwoFactor
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.AppUser
|
||||
SET TwoFactorEnabled = 1,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE UserID = @UserID;
|
||||
|
||||
EXEC dbo.User_GetById @UserID = @UserID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_DisableTwoFactor
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.AppUser
|
||||
SET TwoFactorEnabled = 0,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE UserID = @UserID;
|
||||
|
||||
EXEC dbo.User_GetById @UserID = @UserID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_GetTwoFactor
|
||||
@UserID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT UserID, SecretKey, RecoveryCodes, CreatedUtc
|
||||
FROM dbo.UserTwoFactor
|
||||
WHERE UserID = @UserID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_SaveTwoFactor
|
||||
@UserID int,
|
||||
@SecretKey nvarchar(500),
|
||||
@RecoveryCodes nvarchar(max) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
MERGE dbo.UserTwoFactor AS target
|
||||
USING (SELECT @UserID AS UserID, @SecretKey AS SecretKey, @RecoveryCodes AS RecoveryCodes) AS source
|
||||
ON target.UserID = source.UserID
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET SecretKey = source.SecretKey,
|
||||
RecoveryCodes = source.RecoveryCodes
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (UserID, SecretKey, RecoveryCodes)
|
||||
VALUES (source.UserID, source.SecretKey, source.RecoveryCodes);
|
||||
|
||||
EXEC dbo.User_GetTwoFactor @UserID = @UserID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.User_InsertLoginAudit
|
||||
@UserID int = NULL,
|
||||
@EmailAttempted nvarchar(256) = NULL,
|
||||
@IpAddress nvarchar(100) = NULL,
|
||||
@UserAgent nvarchar(500) = NULL,
|
||||
@WasSuccessful bit,
|
||||
@Reason nvarchar(200) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT INTO dbo.UserLoginAudit (UserID, EmailAttempted, IpAddress, UserAgent, WasSuccessful, Reason)
|
||||
VALUES (@UserID, @EmailAttempted, @IpAddress, @UserAgent, @WasSuccessful, @Reason);
|
||||
|
||||
SELECT CAST(SCOPE_IDENTITY() AS bigint);
|
||||
END;
|
||||
GO
|
||||
100
PlotLine/ViewModels/AuthViewModels.cs
Normal file
100
PlotLine/ViewModels/AuthViewModels.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace PlotLine.ViewModels;
|
||||
|
||||
public sealed class RegisterViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Enter your email address.")]
|
||||
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
|
||||
[StringLength(256, ErrorMessage = "Email must be 256 characters or fewer.")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Enter a display name.")]
|
||||
[StringLength(200, ErrorMessage = "Display name must be 200 characters or fewer.")]
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Enter a password.")]
|
||||
[StringLength(200, MinimumLength = 12, ErrorMessage = "Password must be at least 12 characters.")]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Confirm your password.")]
|
||||
[DataType(DataType.Password)]
|
||||
[Compare(nameof(Password), ErrorMessage = "Passwords do not match.")]
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class LoginViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Enter your email address.")]
|
||||
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
|
||||
[StringLength(256, ErrorMessage = "Email must be 256 characters or fewer.")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Enter your password.")]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
public bool RememberMe { get; set; }
|
||||
|
||||
public string? ReturnUrl { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ForgotPasswordViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Enter your email address.")]
|
||||
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
|
||||
[StringLength(256, ErrorMessage = "Email must be 256 characters or fewer.")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class ResetPasswordViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Enter your email address.")]
|
||||
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
|
||||
[StringLength(256, ErrorMessage = "Email must be 256 characters or fewer.")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Password reset token is missing.")]
|
||||
public Guid Token { get; set; }
|
||||
|
||||
[Required(ErrorMessage = "Enter a new password.")]
|
||||
[StringLength(200, MinimumLength = 12, ErrorMessage = "Password must be at least 12 characters.")]
|
||||
[DataType(DataType.Password)]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Confirm your new password.")]
|
||||
[DataType(DataType.Password)]
|
||||
[Compare(nameof(Password), ErrorMessage = "Passwords do not match.")]
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class TwoFactorViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Enter your authentication code.")]
|
||||
[StringLength(20, ErrorMessage = "Authentication code is too long.")]
|
||||
public string Code { get; set; } = string.Empty;
|
||||
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
public bool RememberMachine { get; set; }
|
||||
|
||||
public bool RememberMe { get; set; }
|
||||
}
|
||||
|
||||
public sealed class ChangePasswordViewModel
|
||||
{
|
||||
[Required(ErrorMessage = "Enter your current password.")]
|
||||
[DataType(DataType.Password)]
|
||||
public string CurrentPassword { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Enter a new password.")]
|
||||
[StringLength(200, MinimumLength = 12, ErrorMessage = "Password must be at least 12 characters.")]
|
||||
[DataType(DataType.Password)]
|
||||
public string NewPassword { get; set; } = string.Empty;
|
||||
|
||||
[Required(ErrorMessage = "Confirm your new password.")]
|
||||
[DataType(DataType.Password)]
|
||||
[Compare(nameof(NewPassword), ErrorMessage = "Passwords do not match.")]
|
||||
public string ConfirmPassword { get; set; } = string.Empty;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user