Initial Commit

master
dan 2019-07-16 21:58:35 -06:00
commit 15ffe475c7
65 changed files with 2975 additions and 0 deletions

275
.gitignore vendored 100644
View File

@ -0,0 +1,275 @@
# Created by https://www.gitignore.io/api/aspnetcore
# Edit at https://www.gitignore.io/?templates=aspnetcore
### ASPNETCore ###
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
# Visual Studio 2015 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# DNX
project.lock.json
project.fragment.lock.json
artifacts/
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding add-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# TODO: Comment the next line if you want to checkin your web deploy settings
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
!**/packages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/packages/repositories.config
# NuGet v3's project.json files produces more ignoreable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# JetBrains Rider
.idea/
*.sln.iml
# CodeRush
.cr/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/
# End of https://www.gitignore.io/api/aspnetcore

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace OFBButte.Api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new string[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}

View File

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OFBButte.Application\OFBButte.Application.csproj" />
<ProjectReference Include="..\OFBButte.Database\OFBButte.Database.csproj" />
<ProjectReference Include="..\OFBButte.Entities\OFBButte.Entities.csproj" />
<ProjectReference Include="..\OFBButte.Infrastructure\OFBButte.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
namespace OFBButte.Api
{
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}

View File

@ -0,0 +1,30 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:50405",
"sslPort": 44314
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"OFBButte.Api": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OFBButte.Application.Configuration;
using OFBButte.Application.Database;
using OFBButte.Database;
namespace OFBButte.Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
// App Settings
services.Configure<AppSettings>(Configuration.GetSection("App"));
// DB Context
services.AddDbContext<OFBContext>(o =>
{
OFBContext.UseMySql(o, Configuration.GetConnectionString("OFBContext"));
});
services.AddScoped<IOFBContext, OFBContext>(s => s.GetService<OFBContext>());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseMvc();
}
}
}

View File

@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"App": {
"Environment": "Dev"
},
"ConnectionStrings": {
"OFBContext": "server=localhost;database=ofbtest;user=ofbapi;password=87hjdusiodksyeunsjkdis7"
}
}

View File

@ -0,0 +1,16 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"App": {
"Environment": "Local"
},
"ConnectionStrings": {
"OFBContext": "server=localhost;database=ofbtest;user=ofbapi;password=87hjdusiodksyeunsjkdis7"
}
}

View File

@ -0,0 +1,39 @@
using OFBButte.Application.Configuration;
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Base
{
public class AuthorizeCommand
{
private readonly IAccess access;
protected readonly IEnumerable<Permission> allowedPermissions;
private AuthorizeCommand(IAccess access)
{
this.access = access;
}
public AuthorizeCommand(IAccess access, Permission permission): this(access)
{
this.allowedPermissions = new Permission[] { permission };
}
public AuthorizeCommand(IAccess access, IEnumerable<Permission> permissions): this(access)
{
this.allowedPermissions = permissions;
}
public void Authorize()
{
if (allowedPermissions == null)
{
throw new UnauthorizedAccessException("Permission Denied");
}
foreach(var permission in allowedPermissions)
{
if (access.CanDo(permission))
return;
}
throw new UnauthorizedAccessException($"Permission Denied: {string.Join(", ", allowedPermissions)}");
}
}
}

View File

@ -0,0 +1,12 @@
using OFBButte.Application.Configuration;
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Base
{
public interface IAccess
{
bool CanDo(Permission permission);
}
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Codes
{
public interface ICodeGenerator
{
string Generate(int byteSize = 16);
}
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Configuration
{
public class AppSettings
{
public string Environment { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Configuration
{
public class ConnectionStrings
{
public string OFBContext { get; set; }
}
}

View File

@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Configuration
{
public class Permission
{
public string Value { get; private set; }
private Permission(string permission)
{
Value = permission;
}
public static Permission AddProfile { get; } = new Permission("Add Profile");
private static Dictionary<string, Permission> values = typeof(Permission).GetProperties(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static).Where(p => p.PropertyType == typeof(Permission)).ToDictionary(p => p.Name, p => (Permission)p.GetValue(null));
public static bool TryCast(string input, out Permission permission)
{
permission = null;
if (values.TryGetValue(input, out Permission perm))
{
permission = perm;
return true;
}
return false;
}
public static implicit operator string(Permission input)
{
return input.Value;
}
public static explicit operator Permission(string input)
{
if (TryCast(input, out Permission permission))
{
return permission;
} else
{
throw new InvalidCastException($"Could not cast {input} to {typeof(Permission).ToString()}");
}
}
}
}

View File

@ -0,0 +1,18 @@
using Microsoft.EntityFrameworkCore;
using OFBButte.Entities;
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Database
{
public interface IOFBContext
{
DbSet<User> Users { get; set; }
DbSet<Profile> Profiles { get; set; }
DbSet<EmailVerificationCode> EmailVerificationCodes { get; set; }
DbSet<PasswordResetCode> PasswordResetCodes { get; set; }
DbSet<MissionarySupport> MissionarySupportForms { get; set; }
int SaveChanges();
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Email
{
public abstract class EmailMessage
{
public virtual string To { get; set; }
public virtual string From { get; set; } = "donotreply@ofbbutte.com";
public virtual string Subject { get; set; }
public virtual string Body { get; set; }
public abstract string TemplateName { get; set; }
public void SetBodyFromTemplate(bool isTest = true)
{
string baseHtml = System.IO.File.ReadAllText($@"Email\Templates\base.html");
string template = System.IO.File.ReadAllText($@"Email\Templates\{TemplateName}.html");
baseHtml = baseHtml.Replace("{{TEST}}", isTest ? "THIS IS A TEST" : "");
template = baseHtml.Replace("{{CONTENT}}", template);
var props = GetType().GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public).ToDictionary(p => p.Name, p => p.GetValue(this)?.ToString() ?? "");
foreach (var property in props)
{
template = template.Replace($"{{{{{property.Key}}}}}", property.Value);
}
Body = template;
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace OFBButte.Application.Email
{
public interface IEmailSender
{
Task Send(EmailMessage message);
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Email
{
public class PasswordResetEmailMessage : EmailMessage
{
public override string TemplateName { get; set; } = "PasswordResetEmailMessage";
public override string Subject { get; set; } = "OFB Butte: Password Reset";
public string Url { get; set; }
}
}

View File

@ -0,0 +1,4 @@
<p>
Please click the link below to reset your password.
</p>
<a href="{{Url}}">{{Url}}</a>

View File

@ -0,0 +1,4 @@
<p>
Please verify your email by clicking the link below.
</p>
<a href="{{VerificationUrl}}">{{VerificationUrl}}</a>

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
{{TEST}}
{{CONTENT}}
</body>
</html>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Email
{
public class VerifyEmailMessage : EmailMessage
{
public override string TemplateName { get; set; } = "VerifyEmailMessage";
public override string Subject { get; set; } = "OFB Butte: Email Verification";
// Email Properties
public string VerificationUrl { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Exceptions
{
public class ValidationException: Exception
{
public ValidationException(string message): base(message)
{
}
}
}

View File

@ -0,0 +1,25 @@
using OFBButte.Application.Database;
using OFBButte.Entities;
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Missionary
{
public class AddMissionarySupportForm
{
private readonly IOFBContext context;
public AddMissionarySupportForm(IOFBContext context)
{
this.context = context;
}
public MissionarySupport Handle(MissionarySupport support)
{
support.Id = 0;
context.MissionarySupportForms.Add(support);
context.SaveChanges();
return support;
}
}
}

View File

@ -0,0 +1,23 @@
using OFBButte.Application.Database;
using OFBButte.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Missionary
{
public class GetMissionarySupportForm
{
private readonly IOFBContext context;
public GetMissionarySupportForm(IOFBContext context)
{
this.context = context;
}
public MissionarySupport Handle(int id)
{
return context.MissionarySupportForms.FirstOrDefault(m => m.Id == id);
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.6" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OFBButte.Entities\OFBButte.Entities.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,55 @@
using OFBButte.Application.Base;
using OFBButte.Application.Codes;
using OFBButte.Application.Configuration;
using OFBButte.Application.Database;
using OFBButte.Entities;
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Users
{
public class AddProfile: AuthorizeCommand
{
private readonly IOFBContext context;
private readonly ICodeGenerator codes;
public AddProfile(IAccess access, IOFBContext context, ICodeGenerator codes)
:base(access, Permission.AddProfile)
{
this.context = context;
this.codes = codes;
}
public Profile Handle(Profile profile)
{
base.Authorize();
return Handle(profile.FirstName, profile.LastName, profile.Street, profile.City, profile.State, profile.Zip, profile.Country);
}
public Profile Handle(string first, string last, string street, string city, string state, string zip, string country)
{
base.Authorize();
var profile = new Profile()
{
FirstName = first,
LastName = last,
Street = street,
City = city,
State = state,
Zip = zip,
Country = country,
ModifiedDate = DateTime.Now,
ProfileFederationCode = new ProfileFederationCode()
{
CreatedDate = DateTime.Now,
Code = codes.Generate()
}
};
context.Profiles.Add(profile);
context.SaveChanges();
return profile;
}
}
}

View File

@ -0,0 +1,42 @@
using OFBButte.Application.Database;
using OFBButte.Application.Exceptions;
using OFBButte.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Users
{
public class AddUser
{
private readonly IOFBContext context;
private readonly IPasswordHasher hasher;
public AddUser(IOFBContext context, IPasswordHasher hasher)
{
this.context = context;
this.hasher = hasher;
}
public User Handle(string email, string password)
{
// Check to see if email already exists
var existingUser = context.Users.FirstOrDefault(u => u.Email == email);
if (existingUser != null)
throw new ValidationException("User with email already exists");
// Create the new user
var user = new Entities.User()
{
Email = email,
CreatedDate = DateTime.Now,
Password = hasher.HashPassword(password)
};
// Save to db
context.Users.Add(user);
context.SaveChanges();
return user;
}
}
}

View File

@ -0,0 +1,27 @@
using OFBButte.Application.Database;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Users
{
public class DeleteUser
{
private readonly IOFBContext context;
public DeleteUser(IOFBContext context)
{
this.context = context;
}
public void Handle(string email)
{
// Get the user
var user = context.Users.FirstOrDefault(u => u.Email == email && u.DeletedDate == null);
if (user == null)
return;
user.DeletedDate = DateTime.Now;
context.SaveChanges();
}
}
}

View File

@ -0,0 +1,34 @@
using OFBButte.Application.Database;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Users
{
public class FederateProfile
{
private readonly IOFBContext context;
public FederateProfile(IOFBContext context)
{
this.context = context;
}
public bool Handle(string email, string lastName, string federationCode)
{
// Get the user
var user = context.Users.FirstOrDefault(u => u.Email == email && u.Profile == null);
if (user == null)
return false;
// Get the profile
var profile = context.Profiles.FirstOrDefault(p => p.ProfileFederationCode.Code == federationCode && p.LastName == lastName);
if (profile == null)
return false;
user.Profile = profile;
context.SaveChanges();
return true;
}
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Application.Users
{
public interface IPasswordHasher
{
string HashPassword(string password);
bool VerifyPassword(string hashedPassword, string password);
}
}

View File

@ -0,0 +1,36 @@
using OFBButte.Application.Database;
using OFBButte.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Users
{
public class LoginUser
{
private readonly IOFBContext context;
private readonly IPasswordHasher hasher;
public LoginUser(IOFBContext context, IPasswordHasher hasher)
{
this.context = context;
this.hasher = hasher;
}
public User Handle(string email, string password)
{
var user = context.Users.FirstOrDefault(u => u.Email == email);
if (user == null)
throw new Exception("Could not find email: " + email);
var validPassword = hasher.VerifyPassword(user.Password, password);
if (validPassword)
{
return user;
} else
{
return null;
}
}
}
}

View File

@ -0,0 +1,34 @@
using OFBButte.Application.Database;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Users
{
public class ResetPassword
{
private readonly IOFBContext context;
private readonly IPasswordHasher hasher;
public ResetPassword(IOFBContext context, IPasswordHasher hasher)
{
this.context = context;
this.hasher = hasher;
}
public bool Handle(int codeId, string code, string newPassword)
{
// Get user
var dte = DateTime.Now.AddHours(-1);
var user = context.Users.FirstOrDefault(u => u.PassswordResetCode.Id == codeId && u.PassswordResetCode.Code == code && u.PassswordResetCode.CreatedDate >= dte);
if (user == null)
return false;
user.Password = hasher.HashPassword(newPassword);
context.PasswordResetCodes.Remove(user.PassswordResetCode);
user.PassswordResetCode = null;
context.SaveChanges();
return true;
}
}
}

View File

@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore;
using OFBButte.Application.Codes;
using OFBButte.Application.Database;
using OFBButte.Application.Email;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace OFBButte.Application.Users
{
public class SendEmailValidation
{
private readonly IOFBContext context;
private readonly IEmailSender emailSender;
private readonly ICodeGenerator codes;
public SendEmailValidation(IOFBContext context, IEmailSender emailSender, ICodeGenerator codes)
{
this.context = context;
this.emailSender = emailSender;
this.codes = codes;
}
public async Task Handle(string email)
{
var user = context.Users.Include(u => u.EmailVerificationCode).FirstOrDefault(u => u.Email == email);
if (user == null)
throw new Exception($"Could not find user with email: {email}");
if (user.EmailVerificationCode != null)
{
context.EmailVerificationCodes.Remove(user.EmailVerificationCode);
}
// Generate verification code
user.EmailVerificationCode = new Entities.EmailVerificationCode()
{
Code = codes.Generate(),
CreatedDate = DateTime.Now
};
context.SaveChanges();
// Create the email
var emailMessage = new VerifyEmailMessage()
{
To = email,
From = "donotreply@ofbbutte.com",
VerificationUrl = $"https://ofbbutte.com/user/emailverification?c={user.EmailVerificationCode.Id}&code={user.EmailVerificationCode.Code}"
};
await emailSender.Send(emailMessage);
}
}
}

View File

@ -0,0 +1,55 @@
using Microsoft.EntityFrameworkCore;
using OFBButte.Application.Codes;
using OFBButte.Application.Database;
using OFBButte.Application.Email;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace OFBButte.Application.Users
{
public class SendResetPasswordEmail
{
private readonly IOFBContext context;
private readonly IEmailSender emailSender;
private readonly ICodeGenerator codes;
public SendResetPasswordEmail(IOFBContext context, IEmailSender emailSender, ICodeGenerator codes)
{
this.context = context;
this.emailSender = emailSender;
this.codes = codes;
}
public async Task Handle(string email, string lastName)
{
// Get the user
var user = context.Users.Include(u => u.PassswordResetCode).FirstOrDefault(u => u.Email == email && u.Profile.LastName == lastName);
if (user == null)
return;
if (user.PassswordResetCode != null)
{
context.PasswordResetCodes.Remove(user.PassswordResetCode);
}
user.PassswordResetCode = new Entities.PasswordResetCode()
{
CreatedDate = DateTime.Now,
Code = codes.Generate()
};
context.SaveChanges();
var message = new PasswordResetEmailMessage()
{
To = user.Email,
Url = $"http://ofbbutte.com/user/resetpassword?c={user.PassswordResetCode.Id}&code={user.PassswordResetCode.Code}"
};
await emailSender.Send(message);
}
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore;
using OFBButte.Application.Database;
using OFBButte.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Users
{
public class UpdateProfile
{
private readonly IOFBContext context;
public UpdateProfile(IOFBContext context)
{
this.context = context;
}
public Profile Handle(string email, Profile profile)
{
// Get the user
var user = context.Users.Include(u => u.Profile).FirstOrDefault(u => u.Email == email);
if (user == null || user.Profile == null)
return null;
user.Profile.City = profile.City;
user.Profile.Country = profile.Country;
user.Profile.FirstName = profile.FirstName;
user.Profile.LastName = profile.LastName;
user.Profile.ModifiedDate = DateTime.Now;
user.Profile.State = profile.State;
user.Profile.Street = profile.Street;
user.Profile.Zip = profile.Zip;
context.SaveChanges();
return user.Profile;
}
}
}

View File

@ -0,0 +1,29 @@
using OFBButte.Application.Database;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace OFBButte.Application.Users
{
public class VerifyEmail
{
private readonly IOFBContext context;
public VerifyEmail(IOFBContext context)
{
this.context = context;
}
public bool Handle(int codeId, string code)
{
var dte = DateTime.Now.AddHours(-1);
var user = context.Users.FirstOrDefault(u => u.EmailVerificationCode.Id == codeId && u.EmailVerificationCode.Code == code && u.EmailVerifiedDate > dte);
if (user == null)
return false;
user.EmailVerifiedDate = DateTime.Now;
context.SaveChanges();
return true;
}
}
}

View File

@ -0,0 +1,61 @@
using Microsoft.Extensions.Options;
using OFBButte.Application.Configuration;
using OFBButte.Application.Database;
using OFBButte.Application.Email;
using OFBButte.Application.Users;
using OFBButte.Infrastructure.Hashing;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace OFBButte.Console
{
public class App
{
private readonly AppSettings appSettings;
private readonly IEmailSender emailSender;
private readonly IOFBContext context;
private string user = "djabsher@gmail.com";
private string pass = "my password";
public App(IOptions<AppSettings> appSettings, IOFBContext context, IEmailSender emailSender)
{
this.appSettings = appSettings.Value;
this.emailSender = emailSender;
this.context = context;
}
public async Task Run()
{
var env = appSettings.Environment;
try
{
await SendVerificationEmail();
}
catch (Exception ex)
{
throw ex;
}
}
private void AddUser()
{
var adder = new AddUser(context, new PasswordHasher());
var result = adder.Handle(user, pass);
}
private async Task SendVerificationEmail()
{
var adder = new SendEmailValidation(context, emailSender, new Infrastructure.Codes.CodeGenerator());
await adder.Handle(user);
}
private void LoginUser()
{
var login = new LoginUser(context, new PasswordHasher());
var result = login.Handle(user, pass);
}
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OFBButte.Application\OFBButte.Application.csproj" />
<ProjectReference Include="..\OFBButte.Database\OFBButte.Database.csproj" />
<ProjectReference Include="..\OFBButte.Infrastructure\OFBButte.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,44 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using OFBButte.Application.Configuration;
using OFBButte.Application.Database;
using OFBButte.Application.Email;
using OFBButte.Database;
using OFBButte.Infrastructure.Email;
using System;
using System.Threading.Tasks;
namespace OFBButte.Console
{
class Program
{
static ServiceProvider services;
static async Task Main(string[] args)
{
SetupDI();
await services.GetService<App>().Run();
}
static void SetupDI()
{
var builder = new ConfigurationBuilder();
builder.AddJsonFile("appsettings.json", false, true);
var config = builder.Build();
var serviceCollection = new ServiceCollection();
serviceCollection.Configure<AppSettings>(a => config.GetSection("app").Bind(a));
serviceCollection.AddSingleton<IEmailSender>(new EmailSender());
serviceCollection.AddDbContext<OFBContext>(o => OFBContext.UseMySql(o, config.GetConnectionString("OFBContext")));
serviceCollection.AddScoped<IOFBContext, OFBContext>(s => s.GetService<OFBContext>());
serviceCollection.AddTransient<App>();
services = serviceCollection.BuildServiceProvider();
}
}
}

View File

@ -0,0 +1,318 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OFBButte.Database;
namespace OFBButte.Database.Migrations
{
[DbContext(typeof(OFBContext))]
[Migration("20190717032510_Initial")]
partial class Initial
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("OFBButte.Entities.EmailVerificationCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Code");
b.Property<DateTime>("CreatedDate");
b.HasKey("Id");
b.ToTable("EmailVerificationCodes");
});
modelBuilder.Entity("OFBButte.Entities.MissionaryChild", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int?>("MissionarySupportId");
b.Property<string>("Name");
b.HasKey("Id");
b.HasIndex("MissionarySupportId");
b.ToTable("MissionaryChild");
});
modelBuilder.Entity("OFBButte.Entities.MissionaryCollegeRecommendation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int?>("MissionarySupportId");
b.Property<string>("Name");
b.HasKey("Id");
b.HasIndex("MissionarySupportId");
b.ToTable("MissionaryCollegeRecommendation");
});
modelBuilder.Entity("OFBButte.Entities.MissionarySupport", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AdmittedWrong");
b.Property<bool>("Alcohol");
b.Property<string>("AloneOrTeam");
b.Property<string>("BibleVersionOpinion");
b.Property<string>("BibleVersionsUsed");
b.Property<string>("BillsOnTime");
b.Property<string>("CallToField");
b.Property<string>("CellPhone");
b.Property<string>("Charasmaticism");
b.Property<int>("ChildrenSchool");
b.Property<bool>("ContemporaryMusic");
b.Property<string>("CorrectWrongOfAnotherMissionary");
b.Property<decimal>("CurrentMonthlySupport");
b.Property<int>("DailyBible");
b.Property<bool>("Dance");
b.Property<bool>("Divorced");
b.Property<string>("EvaluationOfNationals");
b.Property<string>("FellowshipAssociation");
b.Property<bool>("FemaleDressStandard");
b.Property<int>("FemaleShorts");
b.Property<int>("FemaleSlacks");
b.Property<string>("FieldOfService");
b.Property<string>("FieldPhone");
b.Property<string>("FinancialStatementPrevYear");
b.Property<bool>("Fundamentalist");
b.Property<bool>("GroundsForRemarry");
b.Property<string>("HomePhone");
b.Property<string>("LateBillActionTaken");
b.Property<string>("LateBills");
b.Property<bool>("MaleHair");
b.Property<bool>("MarryADivorcee");
b.Property<bool>("MasonicLodge");
b.Property<decimal>("MonthlySupportNeeded");
b.Property<bool>("MovieTheaters");
b.Property<string>("Name");
b.Property<double>("NumberLedToChrist");
b.Property<double>("NumberWeeklyTracts");
b.Property<double>("NumberWitnessedTo");
b.Property<string>("Plans");
b.Property<string>("PotentialHarvest");
b.Property<string>("Predestination");
b.Property<string>("RateOfSuccess");
b.Property<string>("RepentanceDefinition");
b.Property<bool>("RepentanceNecessary");
b.Property<string>("RestAndRelaxation");
b.Property<string>("SendingChurch");
b.Property<bool>("Smoking");
b.Property<string>("SwimmingClothing");
b.Property<string>("Television");
b.Property<string>("Testimony");
b.Property<string>("TimeInCountry");
b.Property<bool>("Tongues");
b.Property<string>("WifesName");
b.Property<bool>("WorldlyMusic");
b.HasKey("Id");
b.ToTable("MissionarySupportForms");
});
modelBuilder.Entity("OFBButte.Entities.PasswordResetCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Code");
b.Property<DateTime>("CreatedDate");
b.HasKey("Id");
b.ToTable("PasswordResetCodes");
});
modelBuilder.Entity("OFBButte.Entities.Profile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("City");
b.Property<string>("Country");
b.Property<string>("FirstName");
b.Property<string>("LastName");
b.Property<DateTime>("ModifiedDate");
b.Property<int?>("ProfileFederationCodeId");
b.Property<string>("State");
b.Property<string>("Street");
b.Property<string>("Zip");
b.HasKey("Id");
b.HasIndex("ProfileFederationCodeId");
b.ToTable("Profiles");
});
modelBuilder.Entity("OFBButte.Entities.ProfileFederationCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Code");
b.Property<DateTime>("CreatedDate");
b.HasKey("Id");
b.ToTable("ProfileFederationCodes");
});
modelBuilder.Entity("OFBButte.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("CreatedDate");
b.Property<DateTime?>("DeletedDate");
b.Property<string>("Email")
.IsRequired();
b.Property<int?>("EmailVerificationCodeId");
b.Property<DateTime?>("EmailVerifiedDate");
b.Property<int?>("PassswordResetCodeId");
b.Property<string>("Password")
.IsRequired();
b.Property<int?>("ProfileId");
b.HasKey("Id");
b.HasIndex("Email");
b.HasIndex("EmailVerificationCodeId");
b.HasIndex("PassswordResetCodeId");
b.HasIndex("ProfileId");
b.ToTable("Users");
});
modelBuilder.Entity("OFBButte.Entities.MissionaryChild", b =>
{
b.HasOne("OFBButte.Entities.MissionarySupport")
.WithMany("Children")
.HasForeignKey("MissionarySupportId");
});
modelBuilder.Entity("OFBButte.Entities.MissionaryCollegeRecommendation", b =>
{
b.HasOne("OFBButte.Entities.MissionarySupport")
.WithMany("CollegeRecommendations")
.HasForeignKey("MissionarySupportId");
});
modelBuilder.Entity("OFBButte.Entities.Profile", b =>
{
b.HasOne("OFBButte.Entities.ProfileFederationCode", "ProfileFederationCode")
.WithMany()
.HasForeignKey("ProfileFederationCodeId");
});
modelBuilder.Entity("OFBButte.Entities.User", b =>
{
b.HasOne("OFBButte.Entities.EmailVerificationCode", "EmailVerificationCode")
.WithMany()
.HasForeignKey("EmailVerificationCodeId");
b.HasOne("OFBButte.Entities.PasswordResetCode", "PassswordResetCode")
.WithMany()
.HasForeignKey("PassswordResetCodeId");
b.HasOne("OFBButte.Entities.Profile", "Profile")
.WithMany()
.HasForeignKey("ProfileId");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,287 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
namespace OFBButte.Database.Migrations
{
public partial class Initial : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "EmailVerificationCodes",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Code = table.Column<string>(nullable: true),
CreatedDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_EmailVerificationCodes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MissionarySupportForms",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(nullable: true),
HomePhone = table.Column<string>(nullable: true),
CellPhone = table.Column<string>(nullable: true),
FieldPhone = table.Column<string>(nullable: true),
WifesName = table.Column<string>(nullable: true),
Testimony = table.Column<string>(nullable: true),
CallToField = table.Column<string>(nullable: true),
SendingChurch = table.Column<string>(nullable: true),
FieldOfService = table.Column<string>(nullable: true),
Plans = table.Column<string>(nullable: true),
EvaluationOfNationals = table.Column<string>(nullable: true),
TimeInCountry = table.Column<string>(nullable: true),
CorrectWrongOfAnotherMissionary = table.Column<string>(nullable: true),
FinancialStatementPrevYear = table.Column<string>(nullable: true),
CurrentMonthlySupport = table.Column<decimal>(nullable: false),
MonthlySupportNeeded = table.Column<decimal>(nullable: false),
RestAndRelaxation = table.Column<string>(nullable: true),
AloneOrTeam = table.Column<string>(nullable: true),
ChildrenSchool = table.Column<int>(nullable: false),
Dance = table.Column<bool>(nullable: false),
WorldlyMusic = table.Column<bool>(nullable: false),
MovieTheaters = table.Column<bool>(nullable: false),
Alcohol = table.Column<bool>(nullable: false),
Smoking = table.Column<bool>(nullable: false),
MaleHair = table.Column<bool>(nullable: false),
FemaleSlacks = table.Column<int>(nullable: false),
FemaleShorts = table.Column<int>(nullable: false),
FemaleDressStandard = table.Column<bool>(nullable: false),
SwimmingClothing = table.Column<string>(nullable: true),
Television = table.Column<string>(nullable: true),
DailyBible = table.Column<int>(nullable: false),
NumberLedToChrist = table.Column<double>(nullable: false),
NumberWitnessedTo = table.Column<double>(nullable: false),
NumberWeeklyTracts = table.Column<double>(nullable: false),
RateOfSuccess = table.Column<string>(nullable: true),
Predestination = table.Column<string>(nullable: true),
FellowshipAssociation = table.Column<string>(nullable: true),
AdmittedWrong = table.Column<string>(nullable: true),
Divorced = table.Column<bool>(nullable: false),
GroundsForRemarry = table.Column<bool>(nullable: false),
MarryADivorcee = table.Column<bool>(nullable: false),
MasonicLodge = table.Column<bool>(nullable: false),
BibleVersionsUsed = table.Column<string>(nullable: true),
BibleVersionOpinion = table.Column<string>(nullable: true),
ContemporaryMusic = table.Column<bool>(nullable: false),
Charasmaticism = table.Column<string>(nullable: true),
Tongues = table.Column<bool>(nullable: false),
RepentanceNecessary = table.Column<bool>(nullable: false),
RepentanceDefinition = table.Column<string>(nullable: true),
Fundamentalist = table.Column<bool>(nullable: false),
BillsOnTime = table.Column<string>(nullable: true),
LateBills = table.Column<string>(nullable: true),
LateBillActionTaken = table.Column<string>(nullable: true),
PotentialHarvest = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MissionarySupportForms", x => x.Id);
});
migrationBuilder.CreateTable(
name: "PasswordResetCodes",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Code = table.Column<string>(nullable: true),
CreatedDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PasswordResetCodes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ProfileFederationCodes",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Code = table.Column<string>(nullable: true),
CreatedDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ProfileFederationCodes", x => x.Id);
});
migrationBuilder.CreateTable(
name: "MissionaryChild",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(nullable: true),
MissionarySupportId = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MissionaryChild", x => x.Id);
table.ForeignKey(
name: "FK_MissionaryChild_MissionarySupportForms_MissionarySupportId",
column: x => x.MissionarySupportId,
principalTable: "MissionarySupportForms",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "MissionaryCollegeRecommendation",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Name = table.Column<string>(nullable: true),
MissionarySupportId = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_MissionaryCollegeRecommendation", x => x.Id);
table.ForeignKey(
name: "FK_MissionaryCollegeRecommendation_MissionarySupportForms_Missi~",
column: x => x.MissionarySupportId,
principalTable: "MissionarySupportForms",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Profiles",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
ModifiedDate = table.Column<DateTime>(nullable: false),
FirstName = table.Column<string>(nullable: true),
LastName = table.Column<string>(nullable: true),
Street = table.Column<string>(nullable: true),
City = table.Column<string>(nullable: true),
State = table.Column<string>(nullable: true),
Zip = table.Column<string>(nullable: true),
Country = table.Column<string>(nullable: true),
ProfileFederationCodeId = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Profiles", x => x.Id);
table.ForeignKey(
name: "FK_Profiles_ProfileFederationCodes_ProfileFederationCodeId",
column: x => x.ProfileFederationCodeId,
principalTable: "ProfileFederationCodes",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
Email = table.Column<string>(nullable: false),
Password = table.Column<string>(nullable: false),
EmailVerifiedDate = table.Column<DateTime>(nullable: true),
CreatedDate = table.Column<DateTime>(nullable: false),
DeletedDate = table.Column<DateTime>(nullable: true),
ProfileId = table.Column<int>(nullable: true),
EmailVerificationCodeId = table.Column<int>(nullable: true),
PassswordResetCodeId = table.Column<int>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
table.ForeignKey(
name: "FK_Users_EmailVerificationCodes_EmailVerificationCodeId",
column: x => x.EmailVerificationCodeId,
principalTable: "EmailVerificationCodes",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Users_PasswordResetCodes_PassswordResetCodeId",
column: x => x.PassswordResetCodeId,
principalTable: "PasswordResetCodes",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
table.ForeignKey(
name: "FK_Users_Profiles_ProfileId",
column: x => x.ProfileId,
principalTable: "Profiles",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_MissionaryChild_MissionarySupportId",
table: "MissionaryChild",
column: "MissionarySupportId");
migrationBuilder.CreateIndex(
name: "IX_MissionaryCollegeRecommendation_MissionarySupportId",
table: "MissionaryCollegeRecommendation",
column: "MissionarySupportId");
migrationBuilder.CreateIndex(
name: "IX_Profiles_ProfileFederationCodeId",
table: "Profiles",
column: "ProfileFederationCodeId");
migrationBuilder.CreateIndex(
name: "IX_Users_Email",
table: "Users",
column: "Email");
migrationBuilder.CreateIndex(
name: "IX_Users_EmailVerificationCodeId",
table: "Users",
column: "EmailVerificationCodeId");
migrationBuilder.CreateIndex(
name: "IX_Users_PassswordResetCodeId",
table: "Users",
column: "PassswordResetCodeId");
migrationBuilder.CreateIndex(
name: "IX_Users_ProfileId",
table: "Users",
column: "ProfileId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "MissionaryChild");
migrationBuilder.DropTable(
name: "MissionaryCollegeRecommendation");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "MissionarySupportForms");
migrationBuilder.DropTable(
name: "EmailVerificationCodes");
migrationBuilder.DropTable(
name: "PasswordResetCodes");
migrationBuilder.DropTable(
name: "Profiles");
migrationBuilder.DropTable(
name: "ProfileFederationCodes");
}
}
}

View File

@ -0,0 +1,316 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using OFBButte.Database;
namespace OFBButte.Database.Migrations
{
[DbContext(typeof(OFBContext))]
partial class OFBContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("OFBButte.Entities.EmailVerificationCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Code");
b.Property<DateTime>("CreatedDate");
b.HasKey("Id");
b.ToTable("EmailVerificationCodes");
});
modelBuilder.Entity("OFBButte.Entities.MissionaryChild", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int?>("MissionarySupportId");
b.Property<string>("Name");
b.HasKey("Id");
b.HasIndex("MissionarySupportId");
b.ToTable("MissionaryChild");
});
modelBuilder.Entity("OFBButte.Entities.MissionaryCollegeRecommendation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<int?>("MissionarySupportId");
b.Property<string>("Name");
b.HasKey("Id");
b.HasIndex("MissionarySupportId");
b.ToTable("MissionaryCollegeRecommendation");
});
modelBuilder.Entity("OFBButte.Entities.MissionarySupport", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("AdmittedWrong");
b.Property<bool>("Alcohol");
b.Property<string>("AloneOrTeam");
b.Property<string>("BibleVersionOpinion");
b.Property<string>("BibleVersionsUsed");
b.Property<string>("BillsOnTime");
b.Property<string>("CallToField");
b.Property<string>("CellPhone");
b.Property<string>("Charasmaticism");
b.Property<int>("ChildrenSchool");
b.Property<bool>("ContemporaryMusic");
b.Property<string>("CorrectWrongOfAnotherMissionary");
b.Property<decimal>("CurrentMonthlySupport");
b.Property<int>("DailyBible");
b.Property<bool>("Dance");
b.Property<bool>("Divorced");
b.Property<string>("EvaluationOfNationals");
b.Property<string>("FellowshipAssociation");
b.Property<bool>("FemaleDressStandard");
b.Property<int>("FemaleShorts");
b.Property<int>("FemaleSlacks");
b.Property<string>("FieldOfService");
b.Property<string>("FieldPhone");
b.Property<string>("FinancialStatementPrevYear");
b.Property<bool>("Fundamentalist");
b.Property<bool>("GroundsForRemarry");
b.Property<string>("HomePhone");
b.Property<string>("LateBillActionTaken");
b.Property<string>("LateBills");
b.Property<bool>("MaleHair");
b.Property<bool>("MarryADivorcee");
b.Property<bool>("MasonicLodge");
b.Property<decimal>("MonthlySupportNeeded");
b.Property<bool>("MovieTheaters");
b.Property<string>("Name");
b.Property<double>("NumberLedToChrist");
b.Property<double>("NumberWeeklyTracts");
b.Property<double>("NumberWitnessedTo");
b.Property<string>("Plans");
b.Property<string>("PotentialHarvest");
b.Property<string>("Predestination");
b.Property<string>("RateOfSuccess");
b.Property<string>("RepentanceDefinition");
b.Property<bool>("RepentanceNecessary");
b.Property<string>("RestAndRelaxation");
b.Property<string>("SendingChurch");
b.Property<bool>("Smoking");
b.Property<string>("SwimmingClothing");
b.Property<string>("Television");
b.Property<string>("Testimony");
b.Property<string>("TimeInCountry");
b.Property<bool>("Tongues");
b.Property<string>("WifesName");
b.Property<bool>("WorldlyMusic");
b.HasKey("Id");
b.ToTable("MissionarySupportForms");
});
modelBuilder.Entity("OFBButte.Entities.PasswordResetCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Code");
b.Property<DateTime>("CreatedDate");
b.HasKey("Id");
b.ToTable("PasswordResetCodes");
});
modelBuilder.Entity("OFBButte.Entities.Profile", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("City");
b.Property<string>("Country");
b.Property<string>("FirstName");
b.Property<string>("LastName");
b.Property<DateTime>("ModifiedDate");
b.Property<int?>("ProfileFederationCodeId");
b.Property<string>("State");
b.Property<string>("Street");
b.Property<string>("Zip");
b.HasKey("Id");
b.HasIndex("ProfileFederationCodeId");
b.ToTable("Profiles");
});
modelBuilder.Entity("OFBButte.Entities.ProfileFederationCode", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<string>("Code");
b.Property<DateTime>("CreatedDate");
b.HasKey("Id");
b.ToTable("ProfileFederationCodes");
});
modelBuilder.Entity("OFBButte.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd();
b.Property<DateTime>("CreatedDate");
b.Property<DateTime?>("DeletedDate");
b.Property<string>("Email")
.IsRequired();
b.Property<int?>("EmailVerificationCodeId");
b.Property<DateTime?>("EmailVerifiedDate");
b.Property<int?>("PassswordResetCodeId");
b.Property<string>("Password")
.IsRequired();
b.Property<int?>("ProfileId");
b.HasKey("Id");
b.HasIndex("Email");
b.HasIndex("EmailVerificationCodeId");
b.HasIndex("PassswordResetCodeId");
b.HasIndex("ProfileId");
b.ToTable("Users");
});
modelBuilder.Entity("OFBButte.Entities.MissionaryChild", b =>
{
b.HasOne("OFBButte.Entities.MissionarySupport")
.WithMany("Children")
.HasForeignKey("MissionarySupportId");
});
modelBuilder.Entity("OFBButte.Entities.MissionaryCollegeRecommendation", b =>
{
b.HasOne("OFBButte.Entities.MissionarySupport")
.WithMany("CollegeRecommendations")
.HasForeignKey("MissionarySupportId");
});
modelBuilder.Entity("OFBButte.Entities.Profile", b =>
{
b.HasOne("OFBButte.Entities.ProfileFederationCode", "ProfileFederationCode")
.WithMany()
.HasForeignKey("ProfileFederationCodeId");
});
modelBuilder.Entity("OFBButte.Entities.User", b =>
{
b.HasOne("OFBButte.Entities.EmailVerificationCode", "EmailVerificationCode")
.WithMany()
.HasForeignKey("EmailVerificationCodeId");
b.HasOne("OFBButte.Entities.PasswordResetCode", "PassswordResetCode")
.WithMany()
.HasForeignKey("PassswordResetCodeId");
b.HasOne("OFBButte.Entities.Profile", "Profile")
.WithMany()
.HasForeignKey("ProfileId");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.2.6" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OFBButte.Application\OFBButte.Application.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,66 @@
using Microsoft.EntityFrameworkCore;
using OFBButte.Application.Database;
using OFBButte.Entities;
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Database
{
public class OFBContext : DbContext, IOFBContext
{
public OFBContext(DbContextOptions<OFBContext> options)
: base(options)
{ }
public static void UseMySql(DbContextOptionsBuilder optionsBuilder, string connString)
{
optionsBuilder.UseMySql(connString);
}
public DbSet<User> Users { get; set; }
public DbSet<Profile> Profiles { get; set; }
public DbSet<ProfileFederationCode> ProfileFederationCodes { get; set; }
public DbSet<EmailVerificationCode> EmailVerificationCodes { get; set; }
public DbSet<PasswordResetCode> PasswordResetCodes { get; set; }
public DbSet<MissionarySupport> MissionarySupportForms { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>(e =>
{
e.HasKey(x => x.Id);
e.HasIndex(x => x.Email);
e.Property(x => x.Email).IsRequired();
e.Property(x => x.Password).IsRequired();
e.Property(x => x.CreatedDate).IsRequired();
e.HasOne(x => x.EmailVerificationCode);
e.HasOne(x => x.Profile);
e.HasOne(x => x.PassswordResetCode);
});
modelBuilder.Entity<EmailVerificationCode>(e => {
e.HasKey(x => x.Id);
});
modelBuilder.Entity<Profile>(e => {
e.HasKey(x => x.Id);
e.HasOne(x => x.ProfileFederationCode);
});
modelBuilder.Entity<ProfileFederationCode>(e => {
e.HasKey(x => x.Id);
});
modelBuilder.Entity<PasswordResetCode>(e => {
e.HasKey(x => x.Id);
});
modelBuilder.Entity<MissionarySupport>(e => {
e.HasKey(x => x.Id);
});
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Entities
{
public class EmailVerificationCode
{
public int Id { get; set; }
public string Code { get; set; }
public DateTime CreatedDate { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace OFBButte.Entities
{
public enum BibleReading
{
Always = 1,
Sometimes = 2,
Rarely = 3
}
}

View File

@ -0,0 +1,10 @@
namespace OFBButte.Entities
{
public enum ChildrenSchool
{
Christian = 1,
Public = 2,
Home = 3,
Other = 5
}
}

View File

@ -0,0 +1,10 @@
namespace OFBButte.Entities
{
public enum FemaleShorts
{
Rarely = 1,
Often = 2,
Sports = 3,
Never = 4
}
}

View File

@ -0,0 +1,10 @@
namespace OFBButte.Entities
{
public enum FemaleSlacks
{
Rarely = 1,
Often = 2,
Sports = 3,
Never = 4
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Entities
{
public class MissionaryChild
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Entities
{
public class MissionaryCollegeRecommendation
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Entities
{
public class MissionarySupport
{
public int Id { get; set; }
public string Name { get; set; }
public string HomePhone { get; set; }
public string CellPhone { get; set; }
public string FieldPhone { get; set; }
public string WifesName { get; set; }
public MissionaryChild[] Children { get; set; }
public string Testimony { get; set; }
public string CallToField { get; set; }
public string SendingChurch { get; set; }
public string FieldOfService { get; set; }
public string Plans { get; set; }
public string EvaluationOfNationals { get; set; }
public string TimeInCountry { get; set; }
public string CorrectWrongOfAnotherMissionary { get; set; }
public string FinancialStatementPrevYear { get; set; }
public decimal CurrentMonthlySupport { get; set; }
public decimal MonthlySupportNeeded { get; set; }
public string RestAndRelaxation { get; set; }
public string AloneOrTeam { get; set; }
public ChildrenSchool ChildrenSchool { get; set; }
public bool Dance { get; set; }
public bool WorldlyMusic { get; set; }
public bool MovieTheaters { get; set; }
public bool Alcohol { get; set; }
public bool Smoking { get; set; }
public bool MaleHair { get; set; }
public FemaleSlacks FemaleSlacks { get; set; }
public FemaleShorts FemaleShorts { get; set; }
public bool FemaleDressStandard { get; set; }
public string SwimmingClothing { get; set; }
public string Television { get; set; }
public BibleReading DailyBible { get; set; }
public double NumberLedToChrist { get; set; }
public double NumberWitnessedTo { get; set; }
public double NumberWeeklyTracts { get; set; }
public string RateOfSuccess { get; set; }
public string Predestination { get; set; }
public string FellowshipAssociation { get; set; }
public MissionaryCollegeRecommendation[] CollegeRecommendations { get; set; }
public string AdmittedWrong { get; set; }
public bool Divorced { get; set; }
public bool GroundsForRemarry { get; set; }
public bool MarryADivorcee { get; set; }
public bool MasonicLodge { get; set; }
public string BibleVersionsUsed { get; set; }
public string BibleVersionOpinion { get; set; }
public bool ContemporaryMusic { get; set; }
public string Charasmaticism { get; set; }
public bool Tongues { get; set; }
public bool RepentanceNecessary { get; set; }
public string RepentanceDefinition { get; set; }
public bool Fundamentalist { get; set; }
public string BillsOnTime { get; set; }
public string LateBills { get; set; }
public string LateBillActionTaken { get; set; }
public string PotentialHarvest { get; set; }
}
}

View File

@ -0,0 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Entities
{
public class PasswordResetCode
{
public int Id { get; set; }
public string Code { get; set; }
public DateTime CreatedDate { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Entities
{
public class Profile
{
public int Id { get; set; }
public DateTime ModifiedDate { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string State { get; set; }
public string Zip { get; set; }
public string Country { get; set; }
public ProfileFederationCode ProfileFederationCode { get; set; }
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Entities
{
public class ProfileFederationCode
{
public int Id { get; set; }
public string Code { get; set; }
public DateTime CreatedDate { get; set; }
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Entities
{
public class User
{
public int Id { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public DateTime? EmailVerifiedDate { get; set; }
public DateTime CreatedDate { get; set; }
public DateTime? DeletedDate { get; set; }
public Profile Profile { get; set; }
public EmailVerificationCode EmailVerificationCode { get; set; }
public PasswordResetCode PassswordResetCode { get; set; }
}
}

View File

@ -0,0 +1,19 @@
using OFBButte.Application.Codes;
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
namespace OFBButte.Infrastructure.Codes
{
public class CodeGenerator: ICodeGenerator
{
public string Generate(int byteSize = 16)
{
byte[] codeBytes = new byte[byteSize];
RandomNumberGenerator.Create().GetBytes(codeBytes);
string code = Convert.ToBase64String(codeBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
return code;
}
}
}

View File

@ -0,0 +1,44 @@
using MailKit.Net.Smtp;
using MimeKit;
using OFBButte.Application.Email;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
namespace OFBButte.Infrastructure.Email
{
public class EmailSender : IEmailSender, IDisposable
{
private readonly SmtpClient client;
private readonly bool isTest;
public EmailSender(bool isTest = true)
{
this.isTest = isTest;
client = new SmtpClient();
}
public async Task Send(EmailMessage message)
{
message.SetBodyFromTemplate(isTest);
var mm = new MimeMessage();
mm.From.Add(new MailboxAddress(message.From));
mm.To.Add(new MailboxAddress(message.To));
mm.Subject = message.Subject;
var bodyBuilder = new BodyBuilder();
bodyBuilder.HtmlBody = message.Body;
mm.Body = bodyBuilder.ToMessageBody();
client.Connect("smtp.webfaction.com", 465, true);
client.Authenticate("ofbcontact", "2014OfbPwd");
await client.SendAsync(mm);
client.Disconnect(true);
}
public void Dispose()
{
client.Dispose();
}
}
}

View File

@ -0,0 +1,215 @@
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using OFBButte.Application.Users;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
namespace OFBButte.Infrastructure.Hashing
{
/// <summary>
/// Implements the standard Identity password hashing.
/// </summary>
public class PasswordHasher: IPasswordHasher
{
/* =======================
* HASHED PASSWORD FORMATS
* =======================
*
* Version 3:
* PBKDF2 with HMAC-SHA256, 128-bit salt, 256-bit subkey, 10000 iterations.
* Format: { 0x01, prf (UInt32), iter count (UInt32), salt length (UInt32), salt, subkey }
* (All UInt32s are stored big-endian.)
*/
private readonly int _iterCount;
private readonly RandomNumberGenerator _rng;
/// <summary>
/// Creates a new instance of <see cref="PasswordHasher{TUser}"/>.
/// </summary>
/// <param name="optionsAccessor">The options for this instance.</param>
public PasswordHasher(PasswordHasherOptions optionsAccessor = null)
{
var options = optionsAccessor ?? new PasswordHasherOptions();
_iterCount = options.IterationCount;
_rng = options.Rng;
}
// Compares two byte arrays for equality. The method is specifically written so that the loop is not optimized.
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
private static bool ByteArraysEqual(byte[] a, byte[] b)
{
if (a == null && b == null)
{
return true;
}
if (a == null || b == null || a.Length != b.Length)
{
return false;
}
var areSame = true;
for (var i = 0; i < a.Length; i++)
{
areSame &= (a[i] == b[i]);
}
return areSame;
}
/// <summary>
/// Returns a hashed representation of the supplied <paramref name="password"/> for the specified <paramref name="user"/>.
/// </summary>
/// <param name="password">The password to hash.</param>
/// <returns>A hashed representation of the supplied <paramref name="password"/> for the specified <paramref name="user"/>.</returns>
public virtual string HashPassword(string password)
{
if (password == null)
{
throw new ArgumentNullException(nameof(password));
}
return Convert.ToBase64String(HashPasswordV3(password, _rng));
}
private byte[] HashPasswordV3(string password, RandomNumberGenerator rng)
{
return HashPasswordV3(password, rng,
prf: KeyDerivationPrf.HMACSHA256,
iterCount: _iterCount,
saltSize: 128 / 8,
numBytesRequested: 256 / 8);
}
private static byte[] HashPasswordV3(string password, RandomNumberGenerator rng, KeyDerivationPrf prf, int iterCount, int saltSize, int numBytesRequested)
{
// Produce a version 3 (see comment above) text hash.
byte[] salt = new byte[saltSize];
rng.GetBytes(salt);
byte[] subkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, numBytesRequested);
var outputBytes = new byte[13 + salt.Length + subkey.Length];
outputBytes[0] = 0x01; // format marker
WriteNetworkByteOrder(outputBytes, 1, (uint)prf);
WriteNetworkByteOrder(outputBytes, 5, (uint)iterCount);
WriteNetworkByteOrder(outputBytes, 9, (uint)saltSize);
Buffer.BlockCopy(salt, 0, outputBytes, 13, salt.Length);
Buffer.BlockCopy(subkey, 0, outputBytes, 13 + saltSize, subkey.Length);
return outputBytes;
}
private static uint ReadNetworkByteOrder(byte[] buffer, int offset)
{
return ((uint)(buffer[offset + 0]) << 24)
| ((uint)(buffer[offset + 1]) << 16)
| ((uint)(buffer[offset + 2]) << 8)
| ((uint)(buffer[offset + 3]));
}
public bool VerifyPassword(string hashedPassword, string password)
{
var result = VerifyHashedPassword(hashedPassword, password);
return result == PasswordVerificationResult.Success;
}
/// <summary>
/// Returns a <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.
/// </summary>
/// <param name="user">The user whose password should be verified.</param>
/// <param name="hashedPassword">The hash value for a user's stored password.</param>
/// <param name="providedPassword">The password supplied for comparison.</param>
/// <returns>A <see cref="PasswordVerificationResult"/> indicating the result of a password hash comparison.</returns>
/// <remarks>Implementations of this method should be time consistent.</remarks>
public virtual PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
if (hashedPassword == null)
{
throw new ArgumentNullException(nameof(hashedPassword));
}
if (providedPassword == null)
{
throw new ArgumentNullException(nameof(providedPassword));
}
byte[] decodedHashedPassword = Convert.FromBase64String(hashedPassword);
// read the format marker from the hashed password
if (decodedHashedPassword.Length == 0)
{
return PasswordVerificationResult.Failed;
}
switch (decodedHashedPassword[0])
{
case 0x00:
return PasswordVerificationResult.Failed;
case 0x01:
int embeddedIterCount;
if (VerifyHashedPasswordV3(decodedHashedPassword, providedPassword, out embeddedIterCount))
{
// If this hasher was configured with a higher iteration count, change the entry now.
return (embeddedIterCount < _iterCount)
? PasswordVerificationResult.SuccessRehashNeeded
: PasswordVerificationResult.Success;
}
else
{
return PasswordVerificationResult.Failed;
}
default:
return PasswordVerificationResult.Failed; // unknown format marker
}
}
private static bool VerifyHashedPasswordV3(byte[] hashedPassword, string password, out int iterCount)
{
iterCount = default(int);
try
{
// Read header information
KeyDerivationPrf prf = (KeyDerivationPrf)ReadNetworkByteOrder(hashedPassword, 1);
iterCount = (int)ReadNetworkByteOrder(hashedPassword, 5);
int saltLength = (int)ReadNetworkByteOrder(hashedPassword, 9);
// Read the salt: must be >= 128 bits
if (saltLength < 128 / 8)
{
return false;
}
byte[] salt = new byte[saltLength];
Buffer.BlockCopy(hashedPassword, 13, salt, 0, salt.Length);
// Read the subkey (the rest of the payload): must be >= 128 bits
int subkeyLength = hashedPassword.Length - 13 - salt.Length;
if (subkeyLength < 128 / 8)
{
return false;
}
byte[] expectedSubkey = new byte[subkeyLength];
Buffer.BlockCopy(hashedPassword, 13 + salt.Length, expectedSubkey, 0, expectedSubkey.Length);
// Hash the incoming password and verify it
byte[] actualSubkey = KeyDerivation.Pbkdf2(password, salt, prf, iterCount, subkeyLength);
return ByteArraysEqual(actualSubkey, expectedSubkey);
}
catch
{
// This should never occur except in the case of a malformed payload, where
// we might go off the end of the array. Regardless, a malformed payload
// implies verification failed.
return false;
}
}
private static void WriteNetworkByteOrder(byte[] buffer, int offset, uint value)
{
buffer[offset + 0] = (byte)(value >> 24);
buffer[offset + 1] = (byte)(value >> 16);
buffer[offset + 2] = (byte)(value >> 8);
buffer[offset + 3] = (byte)(value >> 0);
}
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
namespace OFBButte.Infrastructure.Hashing
{
/// <summary>
/// Specifies options for password hashing.
/// </summary>
public class PasswordHasherOptions
{
private static readonly RandomNumberGenerator _defaultRng = RandomNumberGenerator.Create(); // secure PRNG
/// <summary>
/// Gets or sets the number of iterations used when hashing passwords using PBKDF2. Default is 10,000.
/// </summary>
/// <value>
/// The number of iterations used when hashing passwords using PBKDF2.
/// </value>
/// <remarks>
/// This value is only used when the compatibility mode is set to 'V3'.
/// The value must be a positive integer.
/// </remarks>
public int IterationCount { get; set; } = 10000;
// for unit testing
internal RandomNumberGenerator Rng { get; set; } = _defaultRng;
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace OFBButte.Infrastructure.Hashing
{
// <summary>
/// Specifies the results for password verification.
/// </summary>
public enum PasswordVerificationResult
{
/// <summary>
/// Indicates password verification failed.
/// </summary>
Failed = 0,
/// <summary>
/// Indicates password verification was successful.
/// </summary>
Success = 1,
/// <summary>
/// Indicates password verification was successful however the password was encoded using a deprecated algorithm
/// and should be rehashed and updated.
/// </summary>
SuccessRehashNeeded = 2
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MailKit" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\OFBButte.Application\OFBButte.Application.csproj" />
</ItemGroup>
</Project>

55
OFBButte.sln 100644
View File

@ -0,0 +1,55 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.29102.190
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OFBButte.Api", "OFBButte.Api\OFBButte.Api.csproj", "{FF5F4D78-6419-47E4-A7CE-5B25503BE2D7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OFBButte.Application", "OFBButte.Application\OFBButte.Application.csproj", "{8BCA31C6-C10D-4865-BAE8-6DC932B4797D}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OFBButte.Console", "OFBButte.Console\OFBButte.Console.csproj", "{82FA36EC-1CBC-4F9A-B436-A61D9AA6D1C7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OFBButte.Database", "OFBButte.Database\OFBButte.Database.csproj", "{64A05F44-4BB2-45CE-A343-6307EA8F099A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OFBButte.Infrastructure", "OFBButte.Infrastructure\OFBButte.Infrastructure.csproj", "{157A1CB3-9320-4FE6-ABC5-1C9EF1952540}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OFBButte.Entities", "OFBButte.Entities\OFBButte.Entities.csproj", "{39A5BAF9-E108-4B81-8DBB-A7C1A6CEA9EF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FF5F4D78-6419-47E4-A7CE-5B25503BE2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FF5F4D78-6419-47E4-A7CE-5B25503BE2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF5F4D78-6419-47E4-A7CE-5B25503BE2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FF5F4D78-6419-47E4-A7CE-5B25503BE2D7}.Release|Any CPU.Build.0 = Release|Any CPU
{8BCA31C6-C10D-4865-BAE8-6DC932B4797D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8BCA31C6-C10D-4865-BAE8-6DC932B4797D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8BCA31C6-C10D-4865-BAE8-6DC932B4797D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8BCA31C6-C10D-4865-BAE8-6DC932B4797D}.Release|Any CPU.Build.0 = Release|Any CPU
{82FA36EC-1CBC-4F9A-B436-A61D9AA6D1C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{82FA36EC-1CBC-4F9A-B436-A61D9AA6D1C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{82FA36EC-1CBC-4F9A-B436-A61D9AA6D1C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{82FA36EC-1CBC-4F9A-B436-A61D9AA6D1C7}.Release|Any CPU.Build.0 = Release|Any CPU
{64A05F44-4BB2-45CE-A343-6307EA8F099A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{64A05F44-4BB2-45CE-A343-6307EA8F099A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{64A05F44-4BB2-45CE-A343-6307EA8F099A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{64A05F44-4BB2-45CE-A343-6307EA8F099A}.Release|Any CPU.Build.0 = Release|Any CPU
{157A1CB3-9320-4FE6-ABC5-1C9EF1952540}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{157A1CB3-9320-4FE6-ABC5-1C9EF1952540}.Debug|Any CPU.Build.0 = Debug|Any CPU
{157A1CB3-9320-4FE6-ABC5-1C9EF1952540}.Release|Any CPU.ActiveCfg = Release|Any CPU
{157A1CB3-9320-4FE6-ABC5-1C9EF1952540}.Release|Any CPU.Build.0 = Release|Any CPU
{39A5BAF9-E108-4B81-8DBB-A7C1A6CEA9EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39A5BAF9-E108-4B81-8DBB-A7C1A6CEA9EF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39A5BAF9-E108-4B81-8DBB-A7C1A6CEA9EF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39A5BAF9-E108-4B81-8DBB-A7C1A6CEA9EF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {CD719D07-AABB-44D8-84CD-705EDF224EB9}
EndGlobalSection
EndGlobal