Moved to ASP.NET Core MVC
Thanks to an outstanding presentation from Enrico Sada, I was inspired to use ASP.NET Core MVC for this F# project
This commit is contained in:
parent
1251c28a89
commit
5235e5a5db
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -24,6 +24,7 @@ bld/
|
|||
|
||||
# Visual Studio 2015 cache/options directory
|
||||
.vs/
|
||||
.vscode/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
|
@ -250,3 +251,6 @@ paket-files/
|
|||
# JetBrains Rider
|
||||
.idea/
|
||||
*.sln.iml
|
||||
|
||||
# wwwroot/lib
|
||||
src/MyPrayerJournal/wwwroot/lib
|
3
src/MyPrayerJournal/.bowerrc
Normal file
3
src/MyPrayerJournal/.bowerrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"directory": "wwwroot/lib"
|
||||
}
|
234
src/MyPrayerJournal/.gitignore
vendored
Normal file
234
src/MyPrayerJournal/.gitignore
vendored
Normal file
|
@ -0,0 +1,234 @@
|
|||
## 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,127 +1,79 @@
|
|||
module MyPrayerJournal.App
|
||||
|
||||
open Microsoft.AspNetCore.Builder
|
||||
open Microsoft.AspNetCore.Hosting
|
||||
open Nancy
|
||||
open Nancy.Authentication.Forms
|
||||
open Nancy.Bootstrapper
|
||||
open Nancy.Cryptography
|
||||
open Nancy.Owin
|
||||
open Nancy.Security
|
||||
open Nancy.Session.Persistable
|
||||
open Nancy.Session.RethinkDB
|
||||
open Nancy.TinyIoc
|
||||
open Nancy.ViewEngines.SuperSimpleViewEngine
|
||||
open NodaTime
|
||||
open RethinkDb.Driver.Net
|
||||
open Microsoft.AspNetCore.Localization
|
||||
open Microsoft.Extensions.Configuration
|
||||
open Microsoft.Extensions.DependencyInjection
|
||||
open Microsoft.Extensions.Logging
|
||||
open Microsoft.Extensions.Options
|
||||
open System
|
||||
open System.Reflection
|
||||
open System.Security.Claims
|
||||
open System.Text.RegularExpressions
|
||||
open System.IO
|
||||
|
||||
/// Establish the configuration
|
||||
let cfg = AppConfig.FromJson (System.IO.File.ReadAllText "config.json")
|
||||
|
||||
do
|
||||
cfg.DataConfig.Conn.EstablishEnvironment () |> Async.RunSynchronously
|
||||
|
||||
/// Support i18n/l10n via the @Translate SSVE alias
|
||||
type TranslateTokenViewEngineMatcher() =
|
||||
static let regex = Regex("@Translate\.(?<TranslationKey>[a-zA-Z0-9-_]+);?", RegexOptions.Compiled)
|
||||
interface ISuperSimpleViewEngineMatcher with
|
||||
member this.Invoke (content, model, host) =
|
||||
let translate (m : Match) = Strings.get m.Groups.["TranslationKey"].Value
|
||||
regex.Replace(content, translate)
|
||||
|
||||
/// Handle forms authentication
|
||||
type AppUser(name, claims) =
|
||||
inherit ClaimsPrincipal()
|
||||
member this.UserName with get() = name
|
||||
member this.Claims with get() = claims
|
||||
|
||||
type AppUserMapper(container : TinyIoCContainer) =
|
||||
/// Startup class for myPrayerJournal
|
||||
type Startup(env : IHostingEnvironment) =
|
||||
|
||||
interface IUserMapper with
|
||||
member this.GetUserFromIdentifier (identifier, context) =
|
||||
match context.Request.PersistableSession.GetOrDefault(Keys.User, User.Empty) with
|
||||
| user when user.Id = string identifier -> upcast AppUser(user.Name, [ "LoggedIn" ])
|
||||
| _ -> null
|
||||
/// 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) =
|
||||
ignore <| services.AddOptions ()
|
||||
ignore <| services.Configure<AppConfig>(this.Configuration.GetSection("MyPrayerJournal"))
|
||||
ignore <| services.AddLocalization (fun options -> options.ResourcesPath <- "Resources")
|
||||
ignore <| services.AddMvc ()
|
||||
ignore <| services.AddDistributedMemoryCache ()
|
||||
ignore <| services.AddSession ()
|
||||
// RethinkDB connection
|
||||
async {
|
||||
let cfg = services.BuildServiceProvider().GetService<IOptions<AppConfig>>().Value
|
||||
let! conn = DataConfig.Connect cfg.DataConfig
|
||||
do! conn.EstablishEnvironment cfg
|
||||
ignore <| services.AddSingleton conn
|
||||
} |> Async.RunSynchronously
|
||||
|
||||
/// Set up the application environment
|
||||
type AppBootstrapper() =
|
||||
inherit DefaultNancyBootstrapper()
|
||||
|
||||
override this.ConfigureRequestContainer (container, context) =
|
||||
base.ConfigureRequestContainer (container, context)
|
||||
/// User mapper for forms authentication
|
||||
ignore <| container.Register<IUserMapper, AppUserMapper>()
|
||||
// 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) =
|
||||
ignore <| loggerFactory.AddConsole(this.Configuration.GetSection "Logging")
|
||||
ignore <| loggerFactory.AddDebug ()
|
||||
|
||||
override this.ConfigureApplicationContainer (container) =
|
||||
base.ConfigureApplicationContainer container
|
||||
ignore <| container.Register<AppConfig>(cfg)
|
||||
ignore <| container.Register<IConnection>(cfg.DataConfig.Conn)
|
||||
// NodaTime
|
||||
ignore <| container.Register<IClock>(SystemClock.Instance)
|
||||
// I18N in SSVE
|
||||
ignore <| container.Register<seq<ISuperSimpleViewEngineMatcher>>
|
||||
(fun _ _ ->
|
||||
Seq.singleton (TranslateTokenViewEngineMatcher() :> ISuperSimpleViewEngineMatcher))
|
||||
|
||||
override this.ApplicationStartup (container, pipelines) =
|
||||
base.ApplicationStartup (container, pipelines)
|
||||
// Forms authentication configuration
|
||||
let auth =
|
||||
FormsAuthenticationConfiguration(
|
||||
CryptographyConfiguration =
|
||||
CryptographyConfiguration(
|
||||
AesEncryptionProvider(PassphraseKeyGenerator(cfg.AuthEncryptionPassphrase, cfg.AuthSalt)),
|
||||
DefaultHmacProvider(PassphraseKeyGenerator(cfg.AuthHmacPassphrase, cfg.AuthSalt))),
|
||||
RedirectUrl = "~/user/log-on",
|
||||
UserMapper = container.Resolve<IUserMapper>())
|
||||
FormsAuthentication.Enable (pipelines, auth)
|
||||
// CSRF
|
||||
Csrf.Enable pipelines
|
||||
// Sessions
|
||||
let sessions = RethinkDBSessionConfiguration(cfg.DataConfig.Conn)
|
||||
sessions.Database <- cfg.DataConfig.Database
|
||||
PersistableSessions.Enable (pipelines, sessions)
|
||||
()
|
||||
match env.IsDevelopment () with
|
||||
| true -> ignore <| app.UseDeveloperExceptionPage ()
|
||||
ignore <| app.UseBrowserLink ()
|
||||
| _ -> ignore <| app.UseExceptionHandler("/error")
|
||||
|
||||
override this.Configure (environment) =
|
||||
base.Configure environment
|
||||
environment.Tracing(true, true)
|
||||
ignore <| app.UseStaticFiles ()
|
||||
|
||||
// Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
|
||||
|
||||
let version =
|
||||
let v = typeof<AppConfig>.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"
|
||||
ignore <| app.UseMvc(fun routes ->
|
||||
ignore <| routes.MapRoute(name = "default", template = "{controller=Home}/{action=Index}/{id?}"))
|
||||
|
||||
/// Set up the request environment
|
||||
type RequestEnvironment() =
|
||||
interface IRequestStartup with
|
||||
member this.Initialize (pipelines, context) =
|
||||
pipelines.BeforeRequest.AddItemToStartOfPipeline
|
||||
(fun ctx ->
|
||||
ctx.Items.[Keys.RequestStart] <- DateTime.Now.Ticks
|
||||
ctx.Items.[Keys.Version] <- version
|
||||
null)
|
||||
|
||||
type Startup() =
|
||||
member this.Configure (app : IApplicationBuilder) =
|
||||
ignore <| app.UseOwin(fun x -> x.UseNancy(fun opt -> opt.Bootstrapper <- new AppBootstrapper()) |> ignore)
|
||||
/// Default to Development environment
|
||||
let defaults = seq { yield WebHostDefaults.EnvironmentKey, "Development" }
|
||||
|> dict
|
||||
|
||||
[<EntryPoint>]
|
||||
let main argv =
|
||||
// let app = OwinApp.ofMidFunc "/" (NancyMiddleware.UseNancy(fun opt -> opt.Bootstrapper <- new AppBootstrapper()))
|
||||
// startWebServer defaultConfig app
|
||||
// 0 // return an integer exit code
|
||||
let main argv =
|
||||
let cfg =
|
||||
ConfigurationBuilder()
|
||||
.AddInMemoryCollection(defaults)
|
||||
.AddEnvironmentVariables("ASPNETCORE_")
|
||||
.AddCommandLine(argv)
|
||||
.Build()
|
||||
|
||||
WebHostBuilder()
|
||||
.UseContentRoot(System.IO.Directory.GetCurrentDirectory())
|
||||
.UseConfiguration(cfg)
|
||||
.UseKestrel()
|
||||
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||
.UseStartup<Startup>()
|
||||
.Build()
|
||||
.Run()
|
||||
|
|
|
@ -1,90 +0,0 @@
|
|||
namespace MyPrayerJournal
|
||||
|
||||
open Newtonsoft.Json
|
||||
open RethinkDb.Driver
|
||||
open RethinkDb.Driver.Net
|
||||
open System.Text
|
||||
|
||||
/// Data configuration
|
||||
type DataConfig = {
|
||||
/// The hostname for the RethinkDB server
|
||||
[<JsonProperty("hostname")>]
|
||||
Hostname : string
|
||||
/// The port for the RethinkDB server
|
||||
[<JsonProperty("port")>]
|
||||
Port : int
|
||||
/// The authorization key to use when connecting to the server
|
||||
[<JsonProperty("authKey")>]
|
||||
AuthKey : string
|
||||
/// How long an attempt to connect to the server should wait before giving up
|
||||
[<JsonProperty("timeout")>]
|
||||
Timeout : int
|
||||
/// The name of the default database to use on the connection
|
||||
[<JsonProperty("database")>]
|
||||
Database : string
|
||||
/// A connection to the RethinkDB server using the configuration in this object
|
||||
[<JsonIgnore>]
|
||||
Conn : IConnection
|
||||
}
|
||||
with
|
||||
/// Use RethinkDB defaults for non-provided options, and connect to the server
|
||||
static member Connect config =
|
||||
let ensureHostname cfg = match cfg.Hostname with
|
||||
| null -> { cfg with Hostname = RethinkDBConstants.DefaultHostname }
|
||||
| _ -> cfg
|
||||
let ensurePort cfg = match cfg.Port with
|
||||
| 0 -> { cfg with Port = RethinkDBConstants.DefaultPort }
|
||||
| _ -> cfg
|
||||
let ensureAuthKey cfg = match cfg.AuthKey with
|
||||
| null -> { cfg with AuthKey = RethinkDBConstants.DefaultAuthkey }
|
||||
| _ -> cfg
|
||||
let ensureTimeout cfg = match cfg.Timeout with
|
||||
| 0 -> { cfg with Timeout = RethinkDBConstants.DefaultTimeout }
|
||||
| _ -> cfg
|
||||
let ensureDatabase cfg = match cfg.Database with
|
||||
| null -> { cfg with Database = RethinkDBConstants.DefaultDbName }
|
||||
| _ -> cfg
|
||||
let connect cfg = { cfg with Conn = RethinkDB.R.Connection()
|
||||
.Hostname(cfg.Hostname)
|
||||
.Port(cfg.Port)
|
||||
.AuthKey(cfg.AuthKey)
|
||||
.Db(cfg.Database)
|
||||
.Timeout(cfg.Timeout)
|
||||
.Connect() }
|
||||
config
|
||||
|> ensureHostname
|
||||
|> ensurePort
|
||||
|> ensureAuthKey
|
||||
|> ensureTimeout
|
||||
|> ensureDatabase
|
||||
|> connect
|
||||
|
||||
/// Application configuration
|
||||
type AppConfig = {
|
||||
/// The text from which to derive salt to use for passwords
|
||||
[<JsonProperty("password-salt")>]
|
||||
PasswordSaltString : string
|
||||
/// The text from which to derive salt to use for forms authentication
|
||||
[<JsonProperty("auth-salt")>]
|
||||
AuthSaltString : string
|
||||
/// The encryption passphrase to use for forms authentication
|
||||
[<JsonProperty("encryption-passphrase")>]
|
||||
AuthEncryptionPassphrase : string
|
||||
/// The HMAC passphrase to use for forms authentication
|
||||
[<JsonProperty("hmac-passphrase")>]
|
||||
AuthHmacPassphrase : string
|
||||
/// The data configuration
|
||||
[<JsonProperty("data")>]
|
||||
DataConfig : DataConfig
|
||||
}
|
||||
with
|
||||
/// The salt to use for passwords
|
||||
member this.PasswordSalt = Encoding.UTF8.GetBytes this.PasswordSaltString
|
||||
/// The salt to use for forms authentication
|
||||
member this.AuthSalt = Encoding.UTF8.GetBytes this.AuthSaltString
|
||||
|
||||
/// Deserialize the configuration from the JSON file
|
||||
static member FromJson json =
|
||||
let cfg = JsonConvert.DeserializeObject<AppConfig> json
|
||||
{ cfg with DataConfig = DataConfig.Connect cfg.DataConfig }
|
||||
|
45
src/MyPrayerJournal/Configuration/Configuration.fs
Normal file
45
src/MyPrayerJournal/Configuration/Configuration.fs
Normal file
|
@ -0,0 +1,45 @@
|
|||
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
|
||||
|
12
src/MyPrayerJournal/Configuration/Keys.fs
Normal file
12
src/MyPrayerJournal/Configuration/Keys.fs
Normal file
|
@ -0,0 +1,12 @@
|
|||
/// Magic strings? Look behind the curtain...
|
||||
[<RequireQualifiedAccess>]
|
||||
module MyPrayerJournal.Keys
|
||||
|
||||
/// The current user
|
||||
let CurrentUser = "mpj-user"
|
||||
|
||||
/// The page generator
|
||||
let Generator = "mpj-generator"
|
||||
|
||||
/// The request start ticks
|
||||
let RequestTimer = "mpj-request-timer"
|
30
src/MyPrayerJournal/Controllers/ApplicationController.fs
Normal file
30
src/MyPrayerJournal/Controllers/ApplicationController.fs
Normal file
|
@ -0,0 +1,30 @@
|
|||
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
|
||||
|
12
src/MyPrayerJournal/Controllers/HomeController.fs
Normal file
12
src/MyPrayerJournal/Controllers/HomeController.fs
Normal file
|
@ -0,0 +1,12 @@
|
|||
namespace MyPrayerJournal.Controllers
|
||||
|
||||
open Microsoft.AspNetCore.Mvc
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
/// Home controller
|
||||
[<Route("")>]
|
||||
type HomeController(data : IConnection) =
|
||||
inherit ApplicationController(data)
|
||||
|
||||
[<HttpGet("")>]
|
||||
member this.Index() = this.View()
|
40
src/MyPrayerJournal/Controllers/UserController.fs
Normal file
40
src/MyPrayerJournal/Controllers/UserController.fs
Normal file
|
@ -0,0 +1,40 @@
|
|||
namespace MyPrayerJournal.Controllers
|
||||
|
||||
open Microsoft.AspNetCore.Mvc
|
||||
open Microsoft.Extensions.Options
|
||||
open MyPrayerJournal
|
||||
open MyPrayerJournal.ViewModels
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
/// Controller for all /user URLs
|
||||
[<Route("user")>]
|
||||
type UserController(data : IConnection, cfg : IOptions<AppConfig>) =
|
||||
inherit ApplicationController(data)
|
||||
|
||||
[<HttpGet("log-on")>]
|
||||
member this.ShowLogOn () =
|
||||
this.View(LogOnViewModel())
|
||||
|
||||
|
||||
[<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 -> (* this.Session.[Keys.User] <- usr
|
||||
{ UserMessage.Empty with Level = Level.Info
|
||||
Message = Strings.get "LogOnSuccess" }
|
||||
|> model.AddMessage
|
||||
this.Redirect "" model |> ignore // Save the messages in the session before the Nancy redirect
|
||||
// TODO: investigate if addMessage should update the session when it's called
|
||||
return this.LoginAndRedirect (System.Guid.Parse usr.Id, fallbackRedirectUrl = "/") :> obj
|
||||
*)
|
||||
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")
|
||||
//return this.View()
|
||||
} |> Async.StartAsTask
|
|
@ -1,7 +1,6 @@
|
|||
[<AutoOpen>]
|
||||
module Data
|
||||
module MyPrayerJournal.Data
|
||||
|
||||
open MyPrayerJournal
|
||||
open Newtonsoft.Json
|
||||
open RethinkDb.Driver
|
||||
open RethinkDb.Driver.Ast
|
||||
|
@ -28,59 +27,74 @@ type IConnection with
|
|||
let! user = r.Table(DataTable.User)
|
||||
.GetAll(email).OptArg("index", "Email")
|
||||
.Filter(ReqlFunction1(fun usr -> upcast usr.["PasswordHash"].Eq(passwordHash)))
|
||||
.RunResultAsync<User>(this)
|
||||
|> Async.AwaitTask
|
||||
return match box user with null -> None | _ -> Some user
|
||||
.RunResultAsync<User list>(this)
|
||||
return user |> List.tryHead
|
||||
}
|
||||
|
||||
/// Set up the environment for MyPrayerJournal
|
||||
member this.EstablishEnvironment () =
|
||||
member this.EstablishEnvironment (cfg : AppConfig) =
|
||||
/// Shorthand for the database
|
||||
let db () = r.Db("MyPrayerJournal")
|
||||
/// Log a step in the database environment set up
|
||||
let logStep step = sprintf "[MyPrayerJournal] %s" step |> Console.WriteLine
|
||||
// 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) |> Async.AwaitTask
|
||||
let! dbList = r.DbList().RunResultAsync<string list>(this)
|
||||
match dbList |> List.contains "MyPrayerJournal" with
|
||||
| true -> ()
|
||||
| _ -> logStep " Database not found - creating..."
|
||||
do! r.DbCreate("MyPrayerJournal").RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
|
||||
logStep " ...done"
|
||||
| _ -> 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) |> Async.AwaitTask
|
||||
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 {
|
||||
logStep <| sprintf " %s table not found - creating..." tbl
|
||||
do! db().TableCreate(tbl).RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
|
||||
logStep " ...done"
|
||||
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) |> Async.AwaitTask
|
||||
let! reqIdx = db().Table(DataTable.Request).IndexList().RunResultAsync<string list>(this)
|
||||
match reqIdx |> List.contains "UserId" with
|
||||
| true -> ()
|
||||
| _ -> logStep <| sprintf " %s.UserId index not found - creating..." DataTable.Request
|
||||
do! db().Table(DataTable.Request).IndexCreate("UserId").RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
|
||||
logStep " ...done"
|
||||
let! usrIdx = db().Table(DataTable.User).IndexList().RunResultAsync<string list>(this) |> Async.AwaitTask
|
||||
| _ -> 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 -> ()
|
||||
| _ -> logStep <| sprintf " %s.Email index not found - creating..." DataTable.User
|
||||
do! db().Table(DataTable.User).IndexCreate("Email").RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
|
||||
logStep " ...done"
|
||||
| _ -> 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"
|
|
@ -1,6 +1,7 @@
|
|||
namespace MyPrayerJournal
|
||||
|
||||
open Newtonsoft.Json
|
||||
open System.Security.Cryptography
|
||||
|
||||
/// A user
|
||||
type User = {
|
||||
|
@ -27,8 +28,13 @@ type User = {
|
|||
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
|
19
src/MyPrayerJournal/Helpers/Extensions.fs
Normal file
19
src/MyPrayerJournal/Helpers/Extensions.fs
Normal file
|
@ -0,0 +1,19 @@
|
|||
[<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,15 +0,0 @@
|
|||
namespace MyPrayerJournal
|
||||
|
||||
open Nancy
|
||||
|
||||
type HomeModule() as this =
|
||||
inherit NancyModule()
|
||||
|
||||
do
|
||||
this.Get ("/", fun _ -> this.Home ())
|
||||
|
||||
member this.Home () : obj =
|
||||
let model = MyPrayerJournalModel(this.Context)
|
||||
model.PageTitle <- Strings.get "Welcome"
|
||||
upcast this.View.["home/index", model]
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
[<RequireQualifiedAccess>]
|
||||
module MyPrayerJournal.Keys
|
||||
|
||||
/// Messages stored in the session
|
||||
let Messages = "messages"
|
||||
|
||||
/// The request start time (stored in the context for each request)
|
||||
let RequestStart = "request-start"
|
||||
|
||||
/// The current user
|
||||
let User = "user"
|
||||
|
||||
/// The version of myPrayerJournal
|
||||
let Version = "version"
|
50
src/MyPrayerJournal/Models/ViewModels/AppViewModel.fs
Normal file
50
src/MyPrayerJournal/Models/ViewModels/AppViewModel.fs
Normal file
|
@ -0,0 +1,50 @@
|
|||
namespace MyPrayerJournal.ViewModels
|
||||
|
||||
//open MyPrayerJournal
|
||||
|
||||
/// Parent view model for all myPrayerJournal view models
|
||||
[<AllowNullLiteral>]
|
||||
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 (+)
|
||||
*)
|
17
src/MyPrayerJournal/Models/ViewModels/User/LogOnViewModel.fs
Normal file
17
src/MyPrayerJournal/Models/ViewModels/User/LogOnViewModel.fs
Normal file
|
@ -0,0 +1,17 @@
|
|||
namespace MyPrayerJournal.ViewModels
|
||||
|
||||
open System.ComponentModel.DataAnnotations
|
||||
|
||||
[<AllowNullLiteral>]
|
||||
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,40 +0,0 @@
|
|||
module MyPrayerJournal.Strings
|
||||
|
||||
open Newtonsoft.Json
|
||||
open System.Collections.Generic
|
||||
open System.IO
|
||||
|
||||
/// The locales we'll try to load
|
||||
let private supportedLocales = [ "en-US" ]
|
||||
|
||||
/// The fallback locale, if a key is not found in a non-default locale
|
||||
let private fallbackLocale = "en-US"
|
||||
|
||||
/// Get an embedded JSON file as a string
|
||||
let private getEmbedded locale =
|
||||
use stream = new FileStream((sprintf "resources/%s.json" locale), FileMode.Open)
|
||||
use rdr = new StreamReader(stream)
|
||||
rdr.ReadToEnd()
|
||||
|
||||
/// The dictionary of localized strings
|
||||
let private strings =
|
||||
supportedLocales
|
||||
|> List.map (fun loc -> loc, JsonConvert.DeserializeObject<Dictionary<string, string>>(getEmbedded loc))
|
||||
|> dict
|
||||
|
||||
/// Get a key from the resources file for the given locale
|
||||
let getForLocale locale key =
|
||||
let getString thisLocale =
|
||||
match strings.ContainsKey thisLocale with
|
||||
| true -> match strings.[thisLocale].ContainsKey key with
|
||||
| true -> Some strings.[thisLocale].[key]
|
||||
| _ -> None
|
||||
| _ -> None
|
||||
match getString locale with
|
||||
| Some xlat -> Some xlat
|
||||
| _ when locale <> fallbackLocale -> getString fallbackLocale
|
||||
| _ -> None
|
||||
|> function Some xlat -> xlat | _ -> sprintf "%s.%s" locale key
|
||||
|
||||
/// Translate the key for the current locale
|
||||
let get key = getForLocale System.Globalization.CultureInfo.CurrentCulture.Name key
|
|
@ -1,18 +0,0 @@
|
|||
namespace MyPrayerJournal
|
||||
|
||||
open Nancy
|
||||
|
||||
type UserModule() as this =
|
||||
inherit NancyModule("user")
|
||||
|
||||
do
|
||||
this.Get ("/log-on", fun _ -> this.ShowLogOn ())
|
||||
this.Post("/log-on", fun parms -> this.DoLogOn (downcast parms))
|
||||
|
||||
member this.ShowLogOn () : obj =
|
||||
let model = MyPrayerJournalModel(this.Context)
|
||||
model.PageTitle <- Strings.get "LogOn"
|
||||
upcast this.View.["user/log-on", model]
|
||||
|
||||
member this.DoLogOn (parms : DynamicDictionary) : obj =
|
||||
upcast "X"
|
|
@ -1,140 +0,0 @@
|
|||
namespace MyPrayerJournal
|
||||
|
||||
open Nancy
|
||||
open Nancy.Session.Persistable
|
||||
open Newtonsoft.Json
|
||||
open NodaTime
|
||||
open NodaTime.Text
|
||||
open System
|
||||
|
||||
/// Levels for a user message
|
||||
[<RequireQualifiedAccess>]
|
||||
module Level =
|
||||
/// An informational message
|
||||
let Info = "Info"
|
||||
/// A message regarding a non-fatal but non-optimal condition
|
||||
let Warning = "WARNING"
|
||||
/// A message regarding a failure of the expected result
|
||||
let Error = "ERROR"
|
||||
|
||||
|
||||
/// A message for the user
|
||||
type UserMessage =
|
||||
{ /// The level of the message (use Level module constants)
|
||||
Level : string
|
||||
/// The text of the message
|
||||
Message : string
|
||||
/// Further details regarding the message
|
||||
Details : string option }
|
||||
with
|
||||
/// An empty message
|
||||
static member Empty =
|
||||
{ Level = Level.Info
|
||||
Message = ""
|
||||
Details = None }
|
||||
|
||||
/// Display version
|
||||
[<JsonIgnore>]
|
||||
member this.ToDisplay =
|
||||
let classAndLabel =
|
||||
dict [
|
||||
Level.Error, ("danger", Strings.get "Error")
|
||||
Level.Warning, ("warning", Strings.get "Warning")
|
||||
Level.Info, ("info", "")
|
||||
]
|
||||
seq {
|
||||
yield "<div class=\"alert alert-dismissable alert-"
|
||||
yield fst classAndLabel.[this.Level]
|
||||
yield "\" role=\"alert\"><button type=\"button\" class=\"close\" data-dismiss=\"alert\" aria-label=\""
|
||||
yield Strings.get "Close"
|
||||
yield "\">×</button><strong>"
|
||||
match snd classAndLabel.[this.Level] with
|
||||
| "" -> ()
|
||||
| lbl -> yield lbl.ToUpper ()
|
||||
yield " » "
|
||||
yield this.Message
|
||||
yield "</strong>"
|
||||
match this.Details with
|
||||
| Some d -> yield "<br />"
|
||||
yield d
|
||||
| None -> ()
|
||||
yield "</div>"
|
||||
}
|
||||
|> Seq.reduce (+)
|
||||
|
||||
|
||||
/// Helpers to format local date/time using NodaTime
|
||||
module FormatDateTime =
|
||||
|
||||
/// Convert ticks to a zoned date/time
|
||||
let zonedTime timeZone ticks = Instant.FromUnixTimeTicks(ticks).InZone(DateTimeZoneProviders.Tzdb.[timeZone])
|
||||
|
||||
/// Display a long date
|
||||
let longDate timeZone ticks =
|
||||
zonedTime timeZone ticks
|
||||
|> ZonedDateTimePattern.CreateWithCurrentCulture("MMMM d',' yyyy", DateTimeZoneProviders.Tzdb).Format
|
||||
|
||||
/// Display a short date
|
||||
let shortDate timeZone ticks =
|
||||
zonedTime timeZone ticks
|
||||
|> ZonedDateTimePattern.CreateWithCurrentCulture("MMM d',' yyyy", DateTimeZoneProviders.Tzdb).Format
|
||||
|
||||
/// Display the time
|
||||
let time timeZone ticks =
|
||||
(zonedTime timeZone ticks
|
||||
|> ZonedDateTimePattern.CreateWithCurrentCulture("h':'mmtt", DateTimeZoneProviders.Tzdb).Format).ToLower()
|
||||
|
||||
|
||||
/// Parent view model for all myPrayerJournal view models
|
||||
type MyPrayerJournalModel(ctx : NancyContext) =
|
||||
|
||||
/// Get the messages from the session
|
||||
let getMessages () =
|
||||
let msg = ctx.Request.PersistableSession.GetOrDefault<UserMessage list>(Keys.Messages, [])
|
||||
match List.length msg with
|
||||
| 0 -> ()
|
||||
| _ -> ctx.Request.Session.Delete Keys.Messages
|
||||
msg
|
||||
|
||||
/// User messages
|
||||
member val Messages = getMessages () with get, set
|
||||
/// The currently logged in user
|
||||
member this.User = ctx.Request.PersistableSession.GetOrDefault<User>(Keys.User, User.Empty)
|
||||
/// 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 web log name appended
|
||||
member this.DisplayPageTitle = this.PageTitle (*
|
||||
match this.PageTitle with
|
||||
| "" -> match this.WebLog.Subtitle with
|
||||
| Some st -> sprintf "%s | %s" this.WebLog.Name st
|
||||
| None -> this.WebLog.Name
|
||||
| pt -> sprintf "%s | %s" pt this.WebLog.Name *)
|
||||
|
||||
/// An image with the version and load time in the tool tip
|
||||
member this.FooterLogo =
|
||||
seq {
|
||||
yield "<img src=\"/default/footer-logo.png\" alt=\"myWebLog\" 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 "\" />"
|
||||
}
|
||||
|> Seq.reduce (+)
|
14
src/MyPrayerJournal/Views/Home/Index.cshtml
Normal file
14
src/MyPrayerJournal/Views/Home/Index.cshtml
Normal file
|
@ -0,0 +1,14 @@
|
|||
@{
|
||||
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>
|
14
src/MyPrayerJournal/Views/Shared/Error.cshtml
Normal file
14
src/MyPrayerJournal/Views/Shared/Error.cshtml
Normal file
|
@ -0,0 +1,14 @@
|
|||
@{
|
||||
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>
|
80
src/MyPrayerJournal/Views/Shared/_Layout.cshtml
Normal file
80
src/MyPrayerJournal/Views/Shared/_Layout.cshtml
Normal file
|
@ -0,0 +1,80 @@
|
|||
@{
|
||||
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>
|
|
@ -0,0 +1,14 @@
|
|||
<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>
|
31
src/MyPrayerJournal/Views/User/ShowLogOn.cshtml
Normal file
31
src/MyPrayerJournal/Views/User/ShowLogOn.cshtml
Normal file
|
@ -0,0 +1,31 @@
|
|||
@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>
|
5
src/MyPrayerJournal/Views/_ViewImports.cshtml
Normal file
5
src/MyPrayerJournal/Views/_ViewImports.cshtml
Normal file
|
@ -0,0 +1,5 @@
|
|||
@using Microsoft.Extensions.Localization
|
||||
@using Microsoft.FSharp.Core
|
||||
@using MyPrayerJournal
|
||||
@using MyPrayerJournal.ViewModels
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
3
src/MyPrayerJournal/Views/_ViewStart.cshtml
Normal file
3
src/MyPrayerJournal/Views/_ViewStart.cshtml
Normal file
|
@ -0,0 +1,3 @@
|
|||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
35
src/MyPrayerJournal/appsettings.json
Normal file
35
src/MyPrayerJournal/appsettings.json
Normal file
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
10
src/MyPrayerJournal/bower.json
Normal file
10
src/MyPrayerJournal/bower.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"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,17 +0,0 @@
|
|||
{
|
||||
// https://www.grc.com/passwords.htm is a great source of high-entropy passwords for these first 4 settings.
|
||||
// Although what is there looks strong, keep in mind that it's what's in source control, so all instances of
|
||||
// MyPrayerJournal could be using these values; that severly decreases their usefulness. :)
|
||||
//
|
||||
// WARNING: Changing this first one will render every single user's login inaccessible, including yours. Only do
|
||||
// this if you are editing this file before setting up an instance, or if that is what you intend to do.
|
||||
"password-salt": "oIvatPlrBh5DjeBVWvX3vvePHAgbbzUm7BazZM2IKlUsTtDuPJFbF3KvIiQPdLt",
|
||||
// Changing any of these next 3 will render all current logins invalid, and the user will be force to reauthenticate.
|
||||
"auth-salt": "oQnn82bjs1WysxU0buIdG0DOzsfqIH1pGUnzoavrSDkBIyhWqHffYksRXEAfCup",
|
||||
"encryption-passphrase": "iIIPDrOfke6xP0ElRPMpyMci49CEqBqjmhoxvCiYHZTPSgkTnG2s2m8UOejcFE2",
|
||||
"hmac-passphrase": "0UTc2QfbnHoq4sgTtfkk5JCjoBI2nfm5o0gQsINsBHzwIkU7mAVp61R7UQ1HVrv",
|
||||
"data": {
|
||||
"database": "MyPrayerJournal",
|
||||
"hostname": "severus-server"
|
||||
}
|
||||
}
|
45
src/MyPrayerJournal/gulpfile.js
Normal file
45
src/MyPrayerJournal/gulpfile.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/// <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"]);
|
12
src/MyPrayerJournal/package.json
Normal file
12
src/MyPrayerJournal/package.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"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,50 +1,118 @@
|
|||
{
|
||||
"buildOptions": {
|
||||
"compile": {
|
||||
"includeFiles": [
|
||||
"Entities.fs",
|
||||
"Strings.fs",
|
||||
"Config.fs",
|
||||
"Data.fs",
|
||||
"Keys.fs",
|
||||
"ViewModels.fs",
|
||||
"HomeModule.fs",
|
||||
"UserModule.fs",
|
||||
"App.fs"
|
||||
]
|
||||
},
|
||||
"compilerName": "fsc",
|
||||
"copyToOutput": {
|
||||
"include": [ "views", "resources" ]
|
||||
},
|
||||
"debugType": "portable",
|
||||
"emitEntryPoint": true
|
||||
},
|
||||
"userSecretsId": "aspnet-WebApplication-0799fe3e-6eaf-4c5f-b40e-7c6bfd5dfa9a",
|
||||
|
||||
"dependencies": {
|
||||
"Microsoft.AspNetCore.Hosting": "1.0.0",
|
||||
"Microsoft.AspNetCore.Owin": "1.0.0",
|
||||
"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",
|
||||
"Nancy": "2.0.0-barneyrubble",
|
||||
"Nancy.Authentication.Forms": "2.0.0-barneyrubble",
|
||||
"Nancy.Session.Persistable": "0.9.1-pre",
|
||||
"Nancy.Session.RethinkDB": "0.9.1-pre",
|
||||
"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.Driver": "2.3.15"
|
||||
},
|
||||
"frameworks": {
|
||||
"netcoreapp1.0": {
|
||||
"dependencies": {
|
||||
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160831",
|
||||
"Microsoft.NETCore.App": {
|
||||
"type": "platform",
|
||||
"version": "1.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
"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"
|
||||
]
|
||||
}
|
||||
},
|
||||
"tools": {
|
||||
"dotnet-compile-fsc":"1.0.0-preview2-*"
|
||||
|
||||
"frameworks": {
|
||||
"netcoreapp1.0": {
|
||||
"imports": [
|
||||
"dotnet5.6",
|
||||
"dnxcore50",
|
||||
"portable-net45+win8"
|
||||
]
|
||||
}
|
||||
},
|
||||
"version": "1.0.0-*"
|
||||
|
||||
"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,8 +0,0 @@
|
|||
{
|
||||
"ChangeYourPassword": "Change Your Password",
|
||||
"EmailAddress": "E-mail Address",
|
||||
"LogOff": "Log Off",
|
||||
"LogOn": "Log On",
|
||||
"MyPrayerJournal": "MyPrayerJournal",
|
||||
"Password": "Password"
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
@Master['layout']
|
||||
|
||||
@Section['Content']
|
||||
<p>Hi</p>
|
||||
@EndSection
|
|
@ -1,60 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content="@Model.Generator" />
|
||||
<title>@Model.DisplayPageTitle</title>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
@Section['Head'];
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<nav class="navbar navbar-default">
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-header">
|
||||
<a class="navbar-brand" href="/"><span style="font-weight:100;">My</span><span style="font-weight:600;">Prayer</span><span style="font-weight:700;">Journal</span></a>
|
||||
</div>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
@If.IsAuthenticated
|
||||
<li><a href="/user/change-password">@Translate.ChangeYourPassword</a></li>
|
||||
<li><a href="/user/log-off">@Translate.LogOff</a></li>
|
||||
@EndIf
|
||||
@IfNot.IsAuthenticated
|
||||
<li><a href="/user/log-on">@Translate.LogOn</a></li>
|
||||
@EndIf
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div class="container">
|
||||
@Section['Content'];
|
||||
</div>
|
||||
@Section['Footer'];
|
||||
<script type="text/javascript" src="//ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.3.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||
@Section['Scripts'];
|
||||
</body>
|
||||
</html>
|
|
@ -1,40 +0,0 @@
|
|||
@Master['layout']
|
||||
|
||||
@Section['Content']
|
||||
<form action="/user/log-on" method="post">
|
||||
@AntiForgeryToken
|
||||
<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="@Translate.EmailAddress"><i class="material-icons md-18">email</i></span>
|
||||
<input type="text" name="Email" id="Email" class="form-control" placeholder="@Translate.EmailAddress" />
|
||||
</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="@Translate.Password"><i class="material-icons md-18">security</i></span>
|
||||
<input type="password" name="Password" class="form-control" placeholder="@Translate.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> @Translate.LogOn</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
@EndSection
|
||||
|
||||
@Section['Scripts']
|
||||
<script type="text/javascript">
|
||||
/* <![CDATA[ */
|
||||
$(document).ready(function () { $("#Email").focus() })
|
||||
/* ]]> */
|
||||
</script>
|
||||
@EndSection
|
14
src/MyPrayerJournal/web.config
Normal file
14
src/MyPrayerJournal/web.config
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?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>
|
68
src/MyPrayerJournal/wwwroot/css/site.css
Normal file
68
src/MyPrayerJournal/wwwroot/css/site.css
Normal file
|
@ -0,0 +1,68 @@
|
|||
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;
|
||||
}
|
1
src/MyPrayerJournal/wwwroot/css/site.min.css
vendored
Normal file
1
src/MyPrayerJournal/wwwroot/css/site.min.css
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
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}
|
BIN
src/MyPrayerJournal/wwwroot/favicon.ico
Normal file
BIN
src/MyPrayerJournal/wwwroot/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
1
src/MyPrayerJournal/wwwroot/js/site.js
Normal file
1
src/MyPrayerJournal/wwwroot/js/site.js
Normal file
|
@ -0,0 +1 @@
|
|||
// Write your Javascript code.
|
0
src/MyPrayerJournal/wwwroot/js/site.min.js
vendored
Normal file
0
src/MyPrayerJournal/wwwroot/js/site.min.js
vendored
Normal file
Loading…
Reference in New Issue
Block a user