Switched to Elm / Suave
It's like F# on the client side - sweet!
This commit is contained in:
parent
d3a80b9ceb
commit
bde45c8554
4
.gitignore
vendored
4
.gitignore
vendored
@ -252,5 +252,5 @@ paket-files/
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# wwwroot/lib
|
||||
src/MyPrayerJournal/wwwroot/lib
|
||||
# Elm temporary files
|
||||
src/elm-stuff
|
@ -1,3 +0,0 @@
|
||||
{
|
||||
"directory": "wwwroot/lib"
|
||||
}
|
234
src/MyPrayerJournal/.gitignore
vendored
234
src/MyPrayerJournal/.gitignore
vendored
@ -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/
|
@ -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
|
@ -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
|
||||
|
@ -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"
|
@ -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
|
||||
|
@ -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
|
@ -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
|
@ -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"
|
||||
}
|
@ -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
|
@ -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 = ()
|
@ -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 • " (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 (+)
|
||||
*)
|
@ -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
|
@ -1,14 +0,0 @@
|
||||
@{
|
||||
ViewData["Title"] = "";
|
||||
}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<p> </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. 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. If this is something you are interested in using, check back around mid-November 2016 to check on the
|
||||
development progress.</p>
|
||||
</div>
|
||||
</div>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
@using Microsoft.Extensions.Localization
|
||||
@using Microsoft.FSharp.Core
|
||||
@using MyPrayerJournal
|
||||
@using MyPrayerJournal.ViewModels
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
@ -1,3 +0,0 @@
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"]);
|
@ -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"
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
@ -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>
|
1
src/MyPrayerJournal/wwwroot/css/site.min.css
vendored
1
src/MyPrayerJournal/wwwroot/css/site.min.css
vendored
@ -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 |
@ -1 +0,0 @@
|
||||
// Write your Javascript code.
|
22
src/Program.fs
Normal file
22
src/Program.fs
Normal file
@ -0,0 +1,22 @@
|
||||
// Learn more about F# at http://fsharp.org
|
||||
|
||||
open System.IO
|
||||
open Suave
|
||||
open Suave.Filters
|
||||
open Suave.Operators
|
||||
|
||||
let app : WebPart =
|
||||
choose [
|
||||
//GET >=> path "/" >=> Files.file "index.html"
|
||||
//GET >=> path "" >=> Files.file "index.html"
|
||||
GET >=> Files.browseHome
|
||||
GET >=> Files.browseFileHome "index.html"
|
||||
RequestErrors.NOT_FOUND "Page not found."
|
||||
]
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
let config =
|
||||
{ defaultConfig with homeFolder = Some (Path.GetFullPath "./wwwroot/") }
|
||||
|
||||
startWebServer config app
|
||||
0 // return an integer exit code
|
17
src/elm-package.json
Normal file
17
src/elm-package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"version": "0.8.1",
|
||||
"summary": "A place to record requests, prayers, and answers",
|
||||
"repository": "https://github.com/user/project.git",
|
||||
"license": "MIT",
|
||||
"source-directories": [
|
||||
"wwwroot"
|
||||
],
|
||||
"exposed-modules": [],
|
||||
"dependencies": {
|
||||
"elm-lang/core": "5.0.0 <= v < 6.0.0",
|
||||
"elm-lang/html": "2.0.0 <= v < 3.0.0",
|
||||
"elm-lang/navigation": "2.0.1 <= v < 3.0.0",
|
||||
"evancz/url-parser": "2.0.1 <= v < 3.0.0"
|
||||
},
|
||||
"elm-version": "0.18.0 <= v < 0.19.0"
|
||||
}
|
30
src/project.json
Normal file
30
src/project.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"version": "1.0.0-*",
|
||||
"buildOptions": {
|
||||
"debugType": "portable",
|
||||
"emitEntryPoint": true,
|
||||
"compilerName": "fsc",
|
||||
"compile": {
|
||||
"includeFiles": [
|
||||
"Program.fs"
|
||||
]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"Suave": "2.0.0-rc2"
|
||||
},
|
||||
"tools": {
|
||||
"dotnet-compile-fsc": "1.0.0-preview2.1-*"
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.1": {
|
||||
"dependencies": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"type": "platform",
|
||||
"version": "1.1.0"
|
||||
},
|
||||
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-161111"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
26
src/wwwroot/App.elm
Normal file
26
src/wwwroot/App.elm
Normal file
@ -0,0 +1,26 @@
|
||||
module App exposing (..)
|
||||
|
||||
import Messages exposing (..)
|
||||
import Models exposing (Model, initialModel)
|
||||
import Navigation exposing (Location)
|
||||
import Routing exposing (Route(..), parseLocation)
|
||||
import Update exposing (update)
|
||||
import View exposing (view)
|
||||
|
||||
|
||||
init : Location -> (Model, Cmd Msg)
|
||||
init location =
|
||||
let
|
||||
currentRoute = Home --parseLocation location
|
||||
in
|
||||
(initialModel currentRoute, Cmd.none)
|
||||
|
||||
|
||||
main : Program Never Model Msg
|
||||
main =
|
||||
Navigation.program OnLocationChange
|
||||
{ init = init
|
||||
, view = view
|
||||
, update = update
|
||||
, subscriptions = \_ -> Sub.none
|
||||
}
|
19
src/wwwroot/Home/Public.elm
Normal file
19
src/wwwroot/Home/Public.elm
Normal file
@ -0,0 +1,19 @@
|
||||
module Home.Public exposing (view)
|
||||
|
||||
import Html exposing (Html, p, text)
|
||||
import Messages exposing (Msg(..))
|
||||
import Models exposing (Model)
|
||||
import Utils.View exposing (fullRow)
|
||||
|
||||
|
||||
view : Model -> List (Html Msg)
|
||||
view model =
|
||||
let
|
||||
paragraphs =
|
||||
[ " "
|
||||
, "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. It will also allow individuals to review their answered prayers."
|
||||
, "This site is currently in very limited alpha, as it is being developed with a core group of test users. If this is something you are interested in using, check back around mid-November 2016 to check on the development progress."
|
||||
]
|
||||
|> List.map (\para -> p [] [ text para ])
|
||||
in
|
||||
[ fullRow paragraphs ]
|
9
src/wwwroot/Messages.elm
Normal file
9
src/wwwroot/Messages.elm
Normal file
@ -0,0 +1,9 @@
|
||||
module Messages exposing (..)
|
||||
|
||||
import Navigation exposing (Location)
|
||||
|
||||
|
||||
type Msg
|
||||
= OnLocationChange Location
|
||||
| NavTo String
|
||||
| UpdateTitle String
|
16
src/wwwroot/Models.elm
Normal file
16
src/wwwroot/Models.elm
Normal file
@ -0,0 +1,16 @@
|
||||
module Models exposing (..)
|
||||
|
||||
import Routing exposing (Route(..))
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ route : Route
|
||||
, title : String
|
||||
}
|
||||
|
||||
|
||||
initialModel : Route -> Model
|
||||
initialModel route =
|
||||
{ route = route
|
||||
, title = "Index"
|
||||
}
|
31
src/wwwroot/Routing.elm
Normal file
31
src/wwwroot/Routing.elm
Normal file
@ -0,0 +1,31 @@
|
||||
module Routing exposing (..)
|
||||
|
||||
import Navigation exposing (Location)
|
||||
import UrlParser exposing (..)
|
||||
|
||||
|
||||
type Route
|
||||
= Home
|
||||
| ChangePassword
|
||||
| LogOff
|
||||
| LogOn
|
||||
| NotFound
|
||||
|
||||
|
||||
findRoute : Parser (Route -> a) a
|
||||
findRoute =
|
||||
oneOf
|
||||
[ map Home top
|
||||
, map LogOn (s "user" </> s "log-on")
|
||||
, map LogOff (s "user" </> s "log-off")
|
||||
, map ChangePassword (s "user" </> s "password" </> s "change")
|
||||
]
|
||||
|
||||
|
||||
parseLocation : Location -> Route
|
||||
parseLocation location =
|
||||
case (parsePath findRoute location) of
|
||||
Just route ->
|
||||
route
|
||||
Nothing ->
|
||||
NotFound
|
21
src/wwwroot/Update.elm
Normal file
21
src/wwwroot/Update.elm
Normal file
@ -0,0 +1,21 @@
|
||||
module Update exposing (..)
|
||||
|
||||
import Models exposing (Model)
|
||||
import Messages exposing (Msg(..))
|
||||
import Navigation exposing (newUrl)
|
||||
import Routing exposing (parseLocation)
|
||||
import Utils.View exposing (documentTitle)
|
||||
|
||||
|
||||
update : Msg -> Model -> (Model, Cmd Msg)
|
||||
update msg model =
|
||||
case msg of
|
||||
OnLocationChange location ->
|
||||
let
|
||||
newRoute = parseLocation location
|
||||
in
|
||||
({model | route = newRoute}, Cmd.none)
|
||||
NavTo url ->
|
||||
(model, newUrl url)
|
||||
UpdateTitle newTitle ->
|
||||
(model, documentTitle newTitle)
|
47
src/wwwroot/Utils/View.elm
Normal file
47
src/wwwroot/Utils/View.elm
Normal file
@ -0,0 +1,47 @@
|
||||
port module Utils.View exposing (..)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (class, href, style, title)
|
||||
import Html.Events exposing (defaultOptions, onWithOptions)
|
||||
import Json.Decode as Json
|
||||
import Messages exposing (Msg(..))
|
||||
|
||||
|
||||
-- Set the document title
|
||||
port documentTitle : String -> Cmd a
|
||||
|
||||
|
||||
-- Wrap the given content in a row
|
||||
row : List (Html Msg) -> Html Msg
|
||||
row columns =
|
||||
div [ class "row "] columns
|
||||
|
||||
|
||||
-- Display the given content in a full row
|
||||
fullRow : List (Html Msg) -> Html Msg
|
||||
fullRow content =
|
||||
row
|
||||
[ div
|
||||
[ class "col-xs-12" ]
|
||||
content
|
||||
]
|
||||
|
||||
|
||||
-- Create a navigation link
|
||||
navLink : String -> String -> List (Attribute Msg) -> Html Msg
|
||||
navLink url linkText attrs =
|
||||
let
|
||||
attributes =
|
||||
List.concat
|
||||
[
|
||||
[ title linkText
|
||||
, onWithOptions
|
||||
"click" { defaultOptions | preventDefault = True }
|
||||
<| Json.succeed
|
||||
<| NavTo url
|
||||
, href url
|
||||
]
|
||||
, attrs
|
||||
]
|
||||
in
|
||||
a attributes [ text linkText ]
|
104
src/wwwroot/View.elm
Normal file
104
src/wwwroot/View.elm
Normal file
@ -0,0 +1,104 @@
|
||||
module View exposing (view)
|
||||
|
||||
import Html exposing (..)
|
||||
import Html.Attributes exposing (attribute, class)
|
||||
import Messages exposing (Msg(..))
|
||||
import Models exposing (..)
|
||||
import Routing exposing (Route(..))
|
||||
import Utils.View exposing (documentTitle, navLink)
|
||||
|
||||
import Home.Public
|
||||
|
||||
|
||||
-- Layout functions
|
||||
|
||||
navigation : List (Html Msg)
|
||||
navigation =
|
||||
[ navLink "/user/password/change" "Change Your Password" []
|
||||
, navLink "/user/log-off" "Log Off" []
|
||||
, navLink "/user/log-on" "Log On" []
|
||||
]
|
||||
|> List.map (\anchor -> li [] [ anchor ])
|
||||
|
||||
|
||||
pageHeader : Html Msg
|
||||
pageHeader =
|
||||
div
|
||||
[ class "navbar navbar-inverse navbar-fixed-top" ]
|
||||
[ div
|
||||
[ class "container" ]
|
||||
[ div
|
||||
[ class "navbar-header" ]
|
||||
[ button
|
||||
[ class "navbar-toggle"
|
||||
, attribute "data-toggle" "collapse"
|
||||
, attribute "data-target" ".navbar-collapse"
|
||||
]
|
||||
[ span [ class "sr-only" ] [ text "Toggle navigation" ]
|
||||
, span [ class "icon-bar" ] []
|
||||
, span [ class "icon-bar" ] []
|
||||
, span [ class "icon-bar" ] []
|
||||
]
|
||||
, navLink "/" "myPrayerJournal" [ class "navbar-brand" ]
|
||||
]
|
||||
, div
|
||||
[ class "navbar-collapse collapse" ]
|
||||
[ ul
|
||||
[ class "nav navbar-nav navbar-right" ]
|
||||
navigation
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
pageTitle : String -> Html Msg
|
||||
pageTitle title =
|
||||
let
|
||||
x = documentTitle <| title ++ " | myPrayerJournal"
|
||||
in
|
||||
h2 [ class "page-title" ] [ text title ]
|
||||
|
||||
|
||||
pageFooter : Html Msg
|
||||
pageFooter =
|
||||
footer
|
||||
[ class "mpj-footer" ]
|
||||
[ p
|
||||
[ class "text-right" ]
|
||||
[ text "myPrayerJournal v0.8.1" ]
|
||||
]
|
||||
|
||||
|
||||
layout : Model -> String -> List (Html Msg) -> Html Msg
|
||||
layout model pgTitle contents =
|
||||
let
|
||||
pageContent =
|
||||
[ [ pageTitle pgTitle ]
|
||||
, contents
|
||||
, [ pageFooter ]
|
||||
]
|
||||
|> List.concat
|
||||
in
|
||||
div []
|
||||
[ pageHeader
|
||||
, div
|
||||
[ class "container body-content" ]
|
||||
pageContent
|
||||
]
|
||||
|
||||
|
||||
-- View functions
|
||||
|
||||
view : Model -> Html Msg
|
||||
view model =
|
||||
case model.route of
|
||||
ChangePassword ->
|
||||
layout model "Change Your Password" [ text "password change page goes here" ]
|
||||
Home ->
|
||||
layout model "Welcome" (Home.Public.view model)
|
||||
LogOff ->
|
||||
layout model "Log Off" [ text "Log off page goes hwere" ]
|
||||
LogOn ->
|
||||
layout model "Log On" [ text "Log On page goes here" ]
|
||||
NotFound ->
|
||||
layout model "Page Not Found" [ text "404, dude" ]
|
10330
src/wwwroot/app.js
Normal file
10330
src/wwwroot/app.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -10,39 +10,6 @@ body {
|
||||
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;
|
||||
}
|
24
src/wwwroot/index.html
Normal file
24
src/wwwroot/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>myPrayerJournal</title>
|
||||
<base href="/">
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.6/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="/content/styles.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
|
||||
<script src="/app.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
var app = Elm.App.embed(document.getElementById('app'))
|
||||
app.ports.documentTitle.subscribe(function (title)
|
||||
{
|
||||
alert("Setting title to " + title)
|
||||
document.title = title
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user