Switched to Elm / Suave

It's like F# on the client side - sweet!
This commit is contained in:
Daniel J. Summers
2016-12-11 19:39:06 -06:00
parent d3a80b9ceb
commit bde45c8554
45 changed files with 10698 additions and 1216 deletions

View File

@@ -1,3 +0,0 @@
{
"directory": "wwwroot/lib"
}

View File

@@ -1,234 +0,0 @@
## 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/
build/
bld/
[Bb]in/
[Oo]bj/
# 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
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
# 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
# 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
# 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
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Microsoft Azure ApplicationInsights config file
ApplicationInsights.config
# Windows Store app package directory
AppPackages/
BundleArtifacts/
# 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
*.pfx
*.publishsettings
node_modules/
orleans.codegen.cs
# 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
# FAKE - F# Make
.fake/

View File

@@ -1,92 +0,0 @@
module MyPrayerJournal.App
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.AspNetCore.Localization
open Microsoft.Extensions.Configuration
open Microsoft.Extensions.DependencyInjection
open Microsoft.Extensions.Logging
open Microsoft.Extensions.Options
open RethinkDB.DistributedCache
open System
open System.IO
/// Startup class for myPrayerJournal
type Startup(env : IHostingEnvironment) =
/// Configuration for this application
member this.Configuration =
let builder =
ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional = true, reloadOnChange = true)
.AddJsonFile(sprintf "appsettings.%s.json" env.EnvironmentName, optional = true)
// For more details on using the user secret store see https://go.microsoft.com/fwlink/?LinkID=532709
match env.IsDevelopment () with true -> ignore <| builder.AddUserSecrets () | _ -> ()
ignore <| builder.AddEnvironmentVariables ()
builder.Build ()
// This method gets called by the runtime. Use this method to add services to the container.
member this.ConfigureServices (services : IServiceCollection) =
services.AddOptions () |> ignore
services.Configure<AppConfig> (this.Configuration.GetSection "MyPrayerJournal") |> ignore
services.AddLocalization (fun opt -> opt.ResourcesPath <- "Resources") |> ignore
services.AddMvc () |> ignore
//ignore <| services.AddDistributedMemoryCache ()
// RethinkDB connection
async {
let cfg = services.BuildServiceProvider().GetService<IOptions<AppConfig>>().Value
let! conn = DataConfig.Connect cfg.DataConfig
do! conn.EstablishEnvironment cfg
services.AddSingleton conn |> ignore
services.AddDistributedRethinkDBCache (fun options ->
options.Database <- match cfg.DataConfig.Database with null -> "" | db -> db
options.TableName <- "Session") |> ignore
services.AddSession () |> ignore
} |> Async.RunSynchronously
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
member this.Configure (app : IApplicationBuilder, env : IHostingEnvironment, loggerFactory : ILoggerFactory) =
loggerFactory.AddConsole(this.Configuration.GetSection "Logging") |> ignore
loggerFactory.AddDebug () |> ignore
match env.IsDevelopment () with
| true -> app.UseDeveloperExceptionPage () |> ignore
app.UseBrowserLink () |> ignore
| _ -> app.UseExceptionHandler "/error" |> ignore
app.UseStaticFiles () |> ignore
app.UseCookieAuthentication(
CookieAuthenticationOptions(
AuthenticationScheme = Keys.Authentication,
LoginPath = PathString "/user/log-on",
AutomaticAuthenticate = true,
AutomaticChallenge = true,
ExpireTimeSpan = TimeSpan (2, 0, 0),
SlidingExpiration = true)) |> ignore
app.UseMvc(fun routes ->
routes.MapRoute(name = "default", template = "{controller=Home}/{action=Index}/{id?}") |> ignore) |> ignore
/// Default to Development environment
let defaults = seq { yield WebHostDefaults.EnvironmentKey, "Development" }
|> dict
[<EntryPoint>]
let main argv =
let cfg =
ConfigurationBuilder()
.AddInMemoryCollection(defaults)
.AddEnvironmentVariables("ASPNETCORE_")
.AddCommandLine(argv)
.Build()
use host =
WebHostBuilder()
.UseConfiguration(cfg)
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build()
host.Run()
0

View File

@@ -1,45 +0,0 @@
namespace MyPrayerJournal
open RethinkDb.Driver
open RethinkDb.Driver.Net
open System
open System.Text
/// Data configuration
type DataConfig() =
/// The hostname for the RethinkDB server
member val Hostname = "" with get, set
/// The port for the RethinkDB server
member val Port = 0 with get, set
/// The authorization key to use when connecting to the server
member val AuthKey = "" with get, set
/// How long an attempt to connect to the server should wait before giving up
member val Timeout = 0 with get, set
/// The name of the default database to use on the connection
member val Database = "" with get, set
/// Use RethinkDB defaults for non-provided options, and connect to the server
static member Connect (cfg : DataConfig) =
async {
let builder =
seq<Connection.Builder -> Connection.Builder> {
yield fun b -> if String.IsNullOrEmpty cfg.Hostname then b else b.Hostname cfg.Hostname
yield fun b -> if String.IsNullOrEmpty cfg.AuthKey then b else b.AuthKey cfg.AuthKey
yield fun b -> if String.IsNullOrEmpty cfg.Database then b else b.Db cfg.Database
yield fun b -> if 0 = cfg.Port then b else b.Port cfg.Port
yield fun b -> if 0 = cfg.Timeout then b else b.Timeout cfg.Timeout
}
|> Seq.fold (fun curr block -> block curr) (RethinkDB.R.Connection())
let! conn = builder.ConnectAsync()
return conn :> IConnection
}
/// Application configuration
type AppConfig() =
/// The text from which to derive salt to use for passwords
member val PasswordSalt = "" with get, set
/// The data configuration
member val DataConfig = DataConfig() with get, set
/// The salt to use for passwords
member this.PasswordSaltBytes = Encoding.UTF8.GetBytes this.PasswordSalt

View File

@@ -1,15 +0,0 @@
/// Magic strings? Look behind the curtain...
[<RequireQualifiedAccess>]
module MyPrayerJournal.Keys
/// Instance name for cookie authentication
let Authentication = "mpj-authentication"
/// The current user
let CurrentUser = "mpj-user"
/// The page generator
let Generator = "mpj-generator"
/// The request start ticks
let RequestTimer = "mpj-request-timer"

View File

@@ -1,30 +0,0 @@
namespace MyPrayerJournal.Controllers
open Microsoft.AspNetCore.Mvc
open Microsoft.AspNetCore.Mvc.Filters
open Microsoft.Extensions.Localization
open MyPrayerJournal
open RethinkDb.Driver.Net
open System
open System.Reflection
/// Base controller for all myPrayerJournal controllers
type ApplicationController(data : IConnection) =
inherit Controller()
let version =
let v = typeof<ApplicationController>.GetType().GetTypeInfo().Assembly.GetName().Version
match v.Build with
| 0 -> match v.Minor with 0 -> string v.Major | _ -> sprintf "%d.%d" v.Major v.Minor
| _ -> sprintf "%d.%d.%d" v.Major v.Minor v.Build
|> sprintf "v%s"
/// Fill common items for every request
override this.OnActionExecuting (context : ActionExecutingContext) =
let sw = System.Diagnostics.Stopwatch()
sw.Start()
base.OnActionExecuting context
this.ViewData.[Keys.CurrentUser] <- Option<User>.None
this.ViewData.[Keys.Generator] <- sprintf "myPrayerJournal %s" version
this.ViewData.[Keys.RequestTimer] <- sw

View File

@@ -1,24 +0,0 @@
namespace MyPrayerJournal.Controllers
open Microsoft.AspNetCore.Authorization
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Logging
open MyPrayerJournal
open RethinkDb.Driver.Net
/// Home controller
[<Authorize>]
[<Route("")>]
type HomeController(data : IConnection, logger : ILogger<HomeController>) =
inherit ApplicationController(data)
[<AllowAnonymous>]
[<HttpGet("")>]
member this.Index() =
logger.LogDebug(Newtonsoft.Json.JsonConvert.SerializeObject this.HttpContext.User)
async {
match this.HttpContext.User with
| :? AppUser as user -> return this.View "Dashboard" :> IActionResult
| _ -> return upcast this.View ()
}
|> Async.StartAsTask

View File

@@ -1,49 +0,0 @@
namespace MyPrayerJournal.Controllers
open Microsoft.AspNetCore.Authorization
open Microsoft.AspNetCore.Mvc
open Microsoft.Extensions.Options
open MyPrayerJournal
open MyPrayerJournal.ViewModels
open RethinkDb.Driver.Net
/// Controller for all /user URLs
[<Authorize>]
[<Route("user")>]
type UserController(data : IConnection, cfg : IOptions<AppConfig>) =
inherit ApplicationController(data)
[<AllowAnonymous>]
[<HttpGet("log-on")>]
member this.ShowLogOn () =
this.View(LogOnViewModel())
[<AllowAnonymous>]
[<HttpPost("log-on")>]
[<ValidateAntiForgeryToken>]
member this.DoLogOn (form : LogOnViewModel) =
async {
let! user = data.LogOnUser form.Email (User.HashPassword form.Password cfg.Value.PasswordSaltBytes)
match user with
| Some usr -> do! this.HttpContext.Authentication.SignInAsync (Keys.Authentication, AppUser user)
// TODO: welcome message
(* this.Session.[Keys.User] <- usr
{ UserMessage.Empty with Level = Level.Info
Message = Strings.get "LogOnSuccess" }
|> model.AddMessage *)
return this.Redirect "/" :> IActionResult
| _ -> (*{ UserMessage.Empty with Level = Level.Error
Message = Strings.get "LogOnFailure" }
|> model.AddMessage
return this.Redirect "/user/log-on" model *)
return upcast this.RedirectToAction "ShowLogOn"
}
|> Async.StartAsTask
[<HttpGet("log-off")>]
member this.LogOff () =
async {
do! this.HttpContext.Authentication.SignOutAsync Keys.Authentication
// TODO: goodbye message
return this.LocalRedirect "/"
} |> Async.StartAsTask

View File

@@ -1,106 +0,0 @@
[<AutoOpen>]
module MyPrayerJournal.Data
open Newtonsoft.Json
open RethinkDb.Driver
open RethinkDb.Driver.Ast
open RethinkDb.Driver.Net
open System
let private r = RethinkDB.R
/// Tables for data storage
module DataTable =
/// The table for prayer requests
[<Literal>]
let Request = "Request"
/// The table for users
[<Literal>]
let User = "User"
/// Extensions for the RethinkDB connection
type IConnection with
/// Log on a user
member this.LogOnUser (email : string) (passwordHash : string) =
async {
let! user = r.Table(DataTable.User)
.GetAll(email).OptArg("index", "Email")
.Filter(ReqlFunction1(fun usr -> upcast usr.["PasswordHash"].Eq(passwordHash)))
.RunResultAsync<User list>(this)
return user |> List.tryHead
}
/// Set up the environment for MyPrayerJournal
member this.EstablishEnvironment (cfg : AppConfig) =
/// Shorthand for the database
let db () = r.Db("MyPrayerJournal")
// Be chatty about what we're doing
let mkStep step = sprintf "[MyPrayerJournal] %s" step
let logStep step = mkStep step |> Console.WriteLine
let logStepStart step = mkStep step |> Console.Write
let logStepEnd () = Console.WriteLine " done"
/// Ensure the database exists
let checkDatabase () =
async {
logStep "|> Checking database"
let! dbList = r.DbList().RunResultAsync<string list>(this)
match dbList |> List.contains "MyPrayerJournal" with
| true -> ()
| _ -> logStepStart " Database not found - creating..."
do! r.DbCreate("MyPrayerJournal").RunResultAsync(this)
logStepEnd ()
}
/// Ensure all tables exit
let checkTables () =
async {
logStep "|> Checking tables"
let! tables = db().TableList().RunResultAsync<string list>(this)
[ DataTable.Request; DataTable.User ]
|> List.filter (fun tbl -> not (tables |> List.contains tbl))
|> List.map (fun tbl ->
async {
logStepStart <| sprintf " %s table not found - creating..." tbl
do! db().TableCreate(tbl).RunResultAsync(this)
logStepEnd()
})
|> List.iter Async.RunSynchronously
// Seed the user table if it is empty
let! userCount = db().Table(DataTable.User).Count().RunResultAsync<int64>(this)
match int64 0 = userCount with
| true -> logStepStart " No users found - seeding..."
do! db().Table(DataTable.User).Insert(
{ User.Empty with
Id = Guid.NewGuid().ToString ()
Email = "test@example.com"
PasswordHash = User.HashPassword "password" cfg.PasswordSaltBytes
Name = "Default User"
TimeZone = "America/Chicago"
}).RunResultAsync(this)
logStepEnd ()
| _ -> ()
}
/// Ensure the proper indexes exist
let checkIndexes () =
async {
logStep "|> Checking indexes"
let! reqIdx = db().Table(DataTable.Request).IndexList().RunResultAsync<string list>(this)
match reqIdx |> List.contains "UserId" with
| true -> ()
| _ -> logStepStart <| sprintf " %s.UserId index not found - creating..." DataTable.Request
do! db().Table(DataTable.Request).IndexCreate("UserId").RunResultAsync(this)
logStepEnd ()
let! usrIdx = db().Table(DataTable.User).IndexList().RunResultAsync<string list>(this)
match usrIdx |> List.contains "Email" with
| true -> ()
| _ -> logStepStart <| sprintf " %s.Email index not found - creating..." DataTable.User
do! db().Table(DataTable.User).IndexCreate("Email").RunResultAsync(this)
logStepEnd ()
}
async {
logStep "Database checks starting"
do! checkDatabase ()
do! checkTables ()
do! checkIndexes ()
logStep "Database checks complete"
}

View File

@@ -1,92 +0,0 @@
namespace MyPrayerJournal
open Newtonsoft.Json
open System.Security.Claims
open System.Security.Cryptography
/// A user
type User = {
/// The Id of the user
[<JsonProperty("id")>]
Id : string
/// The user's e-mail address
Email : string
/// A hash of the user's password
PasswordHash : string
/// The user's name
Name : string
/// The time zone in which the user resides
TimeZone : string
/// The last time the user logged on
LastSeenOn : int64
}
with
/// An empty User
static member Empty =
{ Id = ""
Email = ""
PasswordHash = ""
Name = ""
TimeZone = ""
LastSeenOn = int64 0 }
/// Hash a user's password
static member HashPassword (pw : string) (salt : byte[]) =
use hash = new Rfc2898DeriveBytes(pw, salt, 4096)
hash.GetBytes 512
|> Seq.fold (fun acc byt -> sprintf "%s%s" acc (byt.ToString "x2")) ""
/// Request history entry
type History = {
/// The instant at which the update was made
AsOf : int64
/// The action that was taken on the request
Action : string list
/// The status of the request (filled if it changed)
Status : string option
/// The text of the request (filled if it changed)
Text : string option
}
/// A prayer request
type Request = {
/// The Id of the request
[<JsonProperty("id")>]
Id : string
/// The Id of the user to whom this request belongs
UserId : string
/// The instant this request was entered
EnteredOn : int64
/// The history for this request
History : History list
}
with
/// The current status of the prayer request
member this.Status =
this.History
|> List.sortBy (fun item -> -item.AsOf)
|> List.map (fun item -> item.Status)
|> List.filter Option.isSome
|> List.map Option.get
|> List.head
/// The current text of the prayer request
member this.Text =
this.History
|> List.sortBy (fun item -> -item.AsOf)
|> List.map (fun item -> item.Text)
|> List.filter Option.isSome
|> List.map Option.get
|> List.head
member this.LastActionOn =
this.History
|> List.sortBy (fun item -> -item.AsOf)
|> List.map (fun item -> item.AsOf)
|> List.head
/// The user for use with identity
[<AllowNullLiteral>]
type AppUser(user : User option) =
inherit ClaimsPrincipal()
/// The current user
member val User = user with get

View File

@@ -1,19 +0,0 @@
[<AutoOpen>]
module MyPrayerJournal.Extensions
open Microsoft.FSharp.Control
open System.Threading.Tasks
// H/T: Suave
type AsyncBuilder with
/// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
/// a standard .NET task
member x.Bind(t : Task<'T>, f:'T -> Async<'R>) : Async<'R> = async.Bind(Async.AwaitTask t, f)
/// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
/// a standard .NET task which does not commpute a value
member x.Bind(t : Task, f : unit -> Async<'R>) : Async<'R> = async.Bind(Async.AwaitTask t, f)
/// Object to which common IStringLocalizer calls cal be made
type I18N() =
member this.X = ()

View File

@@ -1,49 +0,0 @@
namespace MyPrayerJournal.ViewModels
//open MyPrayerJournal
/// Parent view model for all myPrayerJournal view models
type AppViewModel() =
member this.Q = "X"
(*
/// User messages
member val Messages = getMessages () with get, set
/// The currently logged in user
member val User = Option<User>.None with get, set
/// The title of the page
member val PageTitle = "" with get, set
/// The name and version of the application
member this.Generator = sprintf "myPrayerJournal %s" (ctx.Items.[Keys.Version].ToString ())
/// The request start time
member this.RequestStart = ctx.Items.[Keys.RequestStart] :?> int64
/// Is a user authenticated for this request?
member this.IsAuthenticated = "" <> this.User.Id
/// Add a message to the output
member this.AddMessage message = this.Messages <- message :: this.Messages
/// Display a long date
member this.DisplayLongDate ticks = FormatDateTime.longDate this.User.TimeZone ticks
/// Display a short date
member this.DisplayShortDate ticks = FormatDateTime.shortDate this.User.TimeZone ticks
/// Display the time
member this.DisplayTime ticks = FormatDateTime.time this.User.TimeZone ticks
/// The page title with the application name appended
member this.DisplayPageTitle =
match this.PageTitle with
| "" -> Strings.get "myPrayerJournal"
| pt -> sprintf "%s | %s" pt (Strings.get "myPrayerJournal")
/// An image with the version and load time in the tool tip
member this.FooterLogo =
seq {
yield "<span title=\""
yield sprintf "%s %s &bull; " (Strings.get "PoweredBy") this.Generator
yield Strings.get "LoadedIn"
yield " "
yield TimeSpan(System.DateTime.Now.Ticks - this.RequestStart).TotalSeconds.ToString "f3"
yield " "
yield (Strings.get "Seconds").ToLower ()
yield "\"><span style=\"font-weight:100;\">my</span><span style=\"font-weight:600;\">Prayer</span><span style=\"font-weight:700;\">Journal</span></span>"
}
|> Seq.reduce (+)
*)

View File

@@ -1,16 +0,0 @@
namespace MyPrayerJournal.ViewModels
open System.ComponentModel.DataAnnotations
type LogOnViewModel() =
inherit AppViewModel()
[<Required>]
[<DataType(DataType.EmailAddress)>]
[<Display(Name = "E-mail Address")>]
member val Email = "" with get, set
[<Required>]
[<DataType(DataType.Password)>]
[<Display(Name = "Password")>]
member val Password = "" with get, set

View File

@@ -1,14 +0,0 @@
@{
ViewData["Title"] = "";
}
<div class="row">
<div class="col-xs-12">
<p>&nbsp;</p>
<p>myPrayerJournal is a place where individuals can record their prayer requests, record that they prayed for them,
update them as God moves in the situation, and record a final answer received on that request.&nbsp; It will also
allow individuals to review their answered prayers.</p>
<p>This site is currently in very limited alpha, as it is being developed with a core group of test
users.&nbsp; If this is something you are interested in using, check back around mid-November 2016 to check on the
development progress.</p>
</div>
</div>

View File

@@ -1,14 +0,0 @@
@{
ViewData["Title"] = "Error";
}
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>Development environment should not be enabled in deployed applications</strong>, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>, and restarting the application.
</p>

View File

@@ -1,80 +0,0 @@
@{
var title = ViewData["Title"] as string;
var pageTitle = string.IsNullOrEmpty(title) ? "myPrayerJournal" : String.Format("{0} - myPrayerJournal", title);
var sw = ViewData[Keys.RequestTimer] as System.Diagnostics.Stopwatch;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content="@ViewData[Keys.Generator]" />
<title>@pageTitle</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<environment names="Development">
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment names="Staging,Production">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-controller="Home" asp-action="Index" class="navbar-brand"><span style="font-weight:100;">my</span><span style="font-weight:600;">Prayer</span><span style="font-weight:700;">Journal</span></a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav navbar-right">
@if (OptionModule.IsSome(ViewData[Keys.CurrentUser] as FSharpOption<User>)) {
<li><a asp-controller="User" asp-action="ShowChangePassword">Change Your Password</a></li>
<li><a asp-controller="User" asp-action="LogOff">Log Off</a></li>
}
else {
<li><a asp-controller="User" asp-action="ShowLogOn">Log On</a></li>
}
</ul>
</div>
</div>
</div>
<div class="container body-content">
@if (!string.IsNullOrEmpty(ViewData["Title"] as string)) {
<h1 class="mpj-page-title">@ViewData["Title"]</h1>
}
@RenderBody()
<footer class="mpj-footer">
<p class="text-right" title="Loaded in @sw.Elapsed.ToString(@"s\.fff") seconds">@ViewData[Keys.Generator]</p>
</footer>
</div>
<environment names="Development">
<script src="~/lib/jquery/dist/jquery.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.3.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/bootstrap.min.js"
asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal">
</script>
<script src="~/js/site.min.js" asp-append-version="true"></script>
</environment>
@RenderSection("Scripts", required: false)
</body>
</html>

View File

@@ -1,14 +0,0 @@
<environment names="Development">
<script src="~/lib/jquery-validation/dist/jquery.validate.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js"></script>
</environment>
<environment names="Staging,Production">
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.15.0/jquery.validate.min.js"
asp-fallback-src="~/lib/jquery-validation/dist/jquery.validate.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js"
asp-fallback-src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"
asp-fallback-test="window.jQuery && window.jQuery.validator && window.jQuery.validator.unobtrusive">
</script>
</environment>

View File

@@ -1,40 +0,0 @@
@model LogOnViewModel
@{
ViewData["Title"] = "Log On";
}
<form asp-action="DoLogOn" asp-controller="User" method="post">
<div class="row">
<div class="col-sm-offset-1 col-sm-8 col-md-offset-3 col-md-6">
<div class="input-group">
<span class="input-group-addon" title="E-mail Address"><i class="material-icons md-18">email</i></span>
<input asp-for="Email" class="form-control" placeholder="E-mail Address" />
</div>
</div>
</div>
<div class="row">
<div class="col-sm-offset-1 col-sm-8 col-md-offset-3 col-md-6">
<br />
<div class="input-group">
<span class="input-group-addon" title="Password"><i class="material-icons md-18">security</i></span>
<input asp-for="Password" class="form-control" placeholder="Password" />
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 text-center">
<p>
<br />
<button class="btn btn-primary"><i class="material-icons md-18">verified_user</i> Log On</button>
</p>
</div>
</div>
</form>
@section Scripts
{
<script type="text/javascript">
/* <![CDATA[ */
$(document).ready(function () { $("#Email").focus() })
/* ]]> */
</script>
}

View File

@@ -1,5 +0,0 @@
@using Microsoft.Extensions.Localization
@using Microsoft.FSharp.Core
@using MyPrayerJournal
@using MyPrayerJournal.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@@ -1,3 +0,0 @@
@{
Layout = "_Layout";
}

View File

@@ -1,35 +0,0 @@
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"MyPrayerJournal": {
/*
* myPrayerJournal Configuration
*
* *** SECURITY OPTIONS ***
*
* https://www.grc.com/passwords.htm is a great source of high-entropy passwords. Although what is here looks
* strong, keep in mind that it's what's in source control, so all instances of myPrayerJournal could be using this
* value; that severly decreases its usefulness. :)
*
* WARNING: Changing this will render every single user's login inaccessible, including yours. Do not change it
* once you have started this instance for the first time.
*/
"PasswordSalt": "oIvatPlrBh5DjeBVWvX3vvePHAgbbzUm7BazZM2IKlUsTtDuPJFbF3KvIiQPdLt",
/*
* *** DATA OPTIONS ***
*
* Configure RethinkDB options here; any options not specified will take the driver's default. Available options are
* Hostname (string), Port (int), Database (string), AuthKey (string), and Timeout (int).
*/
"DataConfig": {
"Database": "MyPrayerJournal",
"Hostname": "severus-server"
}
}
}

View File

@@ -1,10 +0,0 @@
{
"name": "myprayerjournal",
"private": true,
"dependencies": {
"bootstrap": "3.3.6",
"jquery": "2.2.3",
"jquery-validation": "1.15.0",
"jquery-validation-unobtrusive": "3.2.6"
}
}

View File

@@ -1,45 +0,0 @@
/// <binding Clean='clean' />
"use strict";
var gulp = require("gulp"),
rimraf = require("rimraf"),
concat = require("gulp-concat"),
cssmin = require("gulp-cssmin"),
uglify = require("gulp-uglify");
var webroot = "./wwwroot/";
var paths = {
js: webroot + "js/**/*.js",
minJs: webroot + "js/**/*.min.js",
css: webroot + "css/**/*.css",
minCss: webroot + "css/**/*.min.css",
concatJsDest: webroot + "js/site.min.js",
concatCssDest: webroot + "css/site.min.css"
};
gulp.task("clean:js", function (cb) {
rimraf(paths.concatJsDest, cb);
});
gulp.task("clean:css", function (cb) {
rimraf(paths.concatCssDest, cb);
});
gulp.task("clean", ["clean:js", "clean:css"]);
gulp.task("min:js", function () {
return gulp.src([paths.js, "!" + paths.minJs], { base: "." })
.pipe(concat(paths.concatJsDest))
.pipe(uglify())
.pipe(gulp.dest("."));
});
gulp.task("min:css", function () {
return gulp.src([paths.css, "!" + paths.minCss])
.pipe(concat(paths.concatCssDest))
.pipe(cssmin())
.pipe(gulp.dest("."));
});
gulp.task("min", ["min:js", "min:css"]);

View File

@@ -1,12 +0,0 @@
{
"name": "myprayerjournal",
"version": "0.8.0",
"private": true,
"devDependencies": {
"gulp": "3.9.1",
"gulp-concat": "2.6.0",
"gulp-cssmin": "0.1.7",
"gulp-uglify": "1.5.3",
"rimraf": "2.5.2"
}
}

View File

@@ -1,119 +0,0 @@
{
"userSecretsId": "aspnet-WebApplication-0799fe3e-6eaf-4c5f-b40e-7c6bfd5dfa9a",
"dependencies": {
"Microsoft.NETCore.App": {
"version": "1.0.1",
"type": "platform"
},
"Microsoft.AspNetCore.Authentication.Cookies": "1.0.0",
"Microsoft.AspNetCore.Diagnostics": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.0.1",
"Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final",
"type": "build"
},
"Microsoft.AspNetCore.Routing": "1.0.1",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
"Microsoft.AspNetCore.Session": "1.0.0",
"Microsoft.AspNetCore.StaticFiles": "1.0.0",
"Microsoft.Extensions.Configuration.CommandLine": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Configuration.UserSecrets": "1.0.0",
"Microsoft.Extensions.Logging": "1.0.0",
"Microsoft.Extensions.Logging.Console": "1.0.0",
"Microsoft.Extensions.Logging.Debug": "1.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160831",
"Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0",
"Microsoft.VisualStudio.Web.CodeGeneration.Tools": {
"version": "1.0.0-preview2-update1",
"type": "build"
},
"Microsoft.VisualStudio.Web.CodeGenerators.Mvc": {
"version": "1.0.0-preview2-update1",
"type": "build"
},
"Newtonsoft.Json": "9.0.1",
"NodaTime": "2.0.0-alpha20160729",
"RethinkDB.DistributedCache": "0.9.0-alpha01",
"RethinkDb.Driver": "2.3.15"
},
"tools": {
"dotnet-compile-fsc":"1.0.0-preview2-*",
"Microsoft.AspNetCore.Razor.Tools": {
"version": "1.0.0-preview2-final",
"imports": "portable-net45+win8+dnxcore50"
},
"Microsoft.Extensions.SecretManager.Tools": {
"version": "1.0.0-preview2-final",
"imports": "portable-net45+win8+dnxcore50"
},
"Microsoft.VisualStudio.Web.CodeGeneration.Tools": {
"version": "1.0.0-preview2-final",
"imports": [
"portable-net45+win8+dnxcore50",
"portable-net45+win8"
]
}
},
"frameworks": {
"netcoreapp1.0": {
"imports": [
"dotnet5.6",
"dnxcore50",
"portable-net45+win8"
]
}
},
"buildOptions": {
"compile": {
"exclude": "**/*",
"includeFiles": [
"Helpers/Extensions.fs",
"Configuration/Configuration.fs",
"Configuration/Keys.fs",
"Data/Entities.fs",
"Data/Data.fs",
"Models/ViewModels/AppViewModel.fs",
"Models/ViewModels/User/LogOnViewModel.fs",
"Controllers/ApplicationController.fs",
"Controllers/HomeController.fs",
"Controllers/UserController.fs",
"App.fs"
]
},
"compilerName": "fsc",
"debugType": "portable",
"emitEntryPoint": true,
"preserveCompilationContext": true
},
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
},
"publishOptions": {
"include": [
"wwwroot",
"**/*.cshtml",
"appsettings.json",
"web.config"
]
},
"scripts": {
"prepublish": [ "npm install", "bower install", "gulp clean", "gulp min" ],
"postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
},
"tooling": {
"defaultNamespace": "MyPrayerJournal"
}
}

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!--
Configure your application settings in appsettings.json. Learn more at https://go.microsoft.com/fwlink/?LinkId=786380
-->
<system.webServer>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false"/>
</system.webServer>
</configuration>

View File

@@ -1,68 +0,0 @@
body {
padding-top: 50px;
padding-bottom: 20px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
}
/* Wrapping element */
/* Set some basic padding to keep content from hitting the edges */
.body-content {
padding-left: 15px;
padding-right: 15px;
}
/* Set widths on the form inputs since otherwise they're 100% wide */
/*input,
select,
textarea {
max-width: 280px;
}*/
/* Carousel */
.carousel-caption p {
font-size: 20px;
line-height: 1.4;
}
/* buttons and links extension to use brackets: [ click me ] */
.btn-bracketed::before {
display:inline-block;
content: "[";
padding-right: 0.5em;
}
.btn-bracketed::after {
display:inline-block;
content: "]";
padding-left: 0.5em;
}
/* Hide/rearrange for smaller screens */
@media screen and (max-width: 767px) {
/* Hide captions */
.carousel-caption {
display: none
}
}
.material-icons.md-18 {
font-size: 18px;
}
.material-icons.md-24 {
font-size: 24px;
}
.material-icons.md-36 {
font-size: 36px;
}
.material-icons.md-48 {
font-size: 48px;
}
.material-icons {
vertical-align: middle;
}
.mpj-page-title {
border-bottom: solid 1px lightgray;
margin-bottom: 20px;
}
.mpj-footer {
border-top: solid 1px lightgray;
margin-top: 20px;
}

View File

@@ -1 +0,0 @@
body{padding-top:50px;padding-bottom:20px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}.body-content{padding-left:15px;padding-right:15px}input,select,textarea{max-width:280px}.carousel-caption p{font-size:20px;line-height:1.4}.btn-bracketed::before{display:inline-block;content:"[";padding-right:.5em}.btn-bracketed::after{display:inline-block;content:"]";padding-left:.5em}@media screen and (max-width:767px){.carousel-caption{display:none}}.material-icons.md-18{font-size:18px}.material-icons.md-24{font-size:24px}.material-icons.md-36{font-size:36px}.material-icons.md-48{font-size:48px}.material-icons{vertical-align:middle}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

@@ -1 +0,0 @@
// Write your Javascript code.