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
|
# Visual Studio 2015 cache/options directory
|
||||||
.vs/
|
.vs/
|
||||||
|
.vscode/
|
||||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
#wwwroot/
|
#wwwroot/
|
||||||
|
|
||||||
@ -250,3 +251,6 @@ paket-files/
|
|||||||
# JetBrains Rider
|
# JetBrains Rider
|
||||||
.idea/
|
.idea/
|
||||||
*.sln.iml
|
*.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.Builder
|
||||||
open Microsoft.AspNetCore.Hosting
|
open Microsoft.AspNetCore.Hosting
|
||||||
open Nancy
|
open Microsoft.AspNetCore.Localization
|
||||||
open Nancy.Authentication.Forms
|
open Microsoft.Extensions.Configuration
|
||||||
open Nancy.Bootstrapper
|
open Microsoft.Extensions.DependencyInjection
|
||||||
open Nancy.Cryptography
|
open Microsoft.Extensions.Logging
|
||||||
open Nancy.Owin
|
open Microsoft.Extensions.Options
|
||||||
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 System
|
open System
|
||||||
open System.Reflection
|
open System.IO
|
||||||
open System.Security.Claims
|
|
||||||
open System.Text.RegularExpressions
|
|
||||||
|
|
||||||
/// Establish the configuration
|
/// Startup class for myPrayerJournal
|
||||||
let cfg = AppConfig.FromJson (System.IO.File.ReadAllText "config.json")
|
type Startup(env : IHostingEnvironment) =
|
||||||
|
|
||||||
do
|
/// Configuration for this application
|
||||||
cfg.DataConfig.Conn.EstablishEnvironment () |> Async.RunSynchronously
|
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 ()
|
||||||
|
|
||||||
/// Support i18n/l10n via the @Translate SSVE alias
|
// This method gets called by the runtime. Use this method to add services to the container.
|
||||||
type TranslateTokenViewEngineMatcher() =
|
member this.ConfigureServices (services : IServiceCollection) =
|
||||||
static let regex = Regex("@Translate\.(?<TranslationKey>[a-zA-Z0-9-_]+);?", RegexOptions.Compiled)
|
ignore <| services.AddOptions ()
|
||||||
interface ISuperSimpleViewEngineMatcher with
|
ignore <| services.Configure<AppConfig>(this.Configuration.GetSection("MyPrayerJournal"))
|
||||||
member this.Invoke (content, model, host) =
|
ignore <| services.AddLocalization (fun options -> options.ResourcesPath <- "Resources")
|
||||||
let translate (m : Match) = Strings.get m.Groups.["TranslationKey"].Value
|
ignore <| services.AddMvc ()
|
||||||
regex.Replace(content, translate)
|
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
|
||||||
|
|
||||||
/// Handle forms authentication
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
type AppUser(name, claims) =
|
member this.Configure (app : IApplicationBuilder, env : IHostingEnvironment, loggerFactory : ILoggerFactory) =
|
||||||
inherit ClaimsPrincipal()
|
ignore <| loggerFactory.AddConsole(this.Configuration.GetSection "Logging")
|
||||||
member this.UserName with get() = name
|
ignore <| loggerFactory.AddDebug ()
|
||||||
member this.Claims with get() = claims
|
|
||||||
|
|
||||||
type AppUserMapper(container : TinyIoCContainer) =
|
match env.IsDevelopment () with
|
||||||
|
| true -> ignore <| app.UseDeveloperExceptionPage ()
|
||||||
|
ignore <| app.UseBrowserLink ()
|
||||||
|
| _ -> ignore <| app.UseExceptionHandler("/error")
|
||||||
|
|
||||||
interface IUserMapper with
|
ignore <| app.UseStaticFiles ()
|
||||||
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
|
|
||||||
|
|
||||||
|
// Add external authentication middleware below. To configure them please see https://go.microsoft.com/fwlink/?LinkID=532715
|
||||||
|
|
||||||
/// Set up the application environment
|
ignore <| app.UseMvc(fun routes ->
|
||||||
type AppBootstrapper() =
|
ignore <| routes.MapRoute(name = "default", template = "{controller=Home}/{action=Index}/{id?}"))
|
||||||
inherit DefaultNancyBootstrapper()
|
|
||||||
|
|
||||||
override this.ConfigureRequestContainer (container, context) =
|
/// Default to Development environment
|
||||||
base.ConfigureRequestContainer (container, context)
|
let defaults = seq { yield WebHostDefaults.EnvironmentKey, "Development" }
|
||||||
/// User mapper for forms authentication
|
|> dict
|
||||||
ignore <| container.Register<IUserMapper, AppUserMapper>()
|
|
||||||
|
|
||||||
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)
|
|
||||||
()
|
|
||||||
|
|
||||||
override this.Configure (environment) =
|
|
||||||
base.Configure environment
|
|
||||||
environment.Tracing(true, true)
|
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
|
|
||||||
/// 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)
|
|
||||||
|
|
||||||
[<EntryPoint>]
|
[<EntryPoint>]
|
||||||
let main argv =
|
let main argv =
|
||||||
// let app = OwinApp.ofMidFunc "/" (NancyMiddleware.UseNancy(fun opt -> opt.Bootstrapper <- new AppBootstrapper()))
|
let cfg =
|
||||||
// startWebServer defaultConfig app
|
ConfigurationBuilder()
|
||||||
// 0 // return an integer exit code
|
.AddInMemoryCollection(defaults)
|
||||||
|
.AddEnvironmentVariables("ASPNETCORE_")
|
||||||
|
.AddCommandLine(argv)
|
||||||
|
.Build()
|
||||||
|
|
||||||
WebHostBuilder()
|
WebHostBuilder()
|
||||||
.UseContentRoot(System.IO.Directory.GetCurrentDirectory())
|
.UseConfiguration(cfg)
|
||||||
.UseKestrel()
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
.UseStartup<Startup>()
|
.UseStartup<Startup>()
|
||||||
.Build()
|
.Build()
|
||||||
.Run()
|
.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>]
|
[<AutoOpen>]
|
||||||
module Data
|
module MyPrayerJournal.Data
|
||||||
|
|
||||||
open MyPrayerJournal
|
|
||||||
open Newtonsoft.Json
|
open Newtonsoft.Json
|
||||||
open RethinkDb.Driver
|
open RethinkDb.Driver
|
||||||
open RethinkDb.Driver.Ast
|
open RethinkDb.Driver.Ast
|
||||||
@ -28,59 +27,74 @@ type IConnection with
|
|||||||
let! user = r.Table(DataTable.User)
|
let! user = r.Table(DataTable.User)
|
||||||
.GetAll(email).OptArg("index", "Email")
|
.GetAll(email).OptArg("index", "Email")
|
||||||
.Filter(ReqlFunction1(fun usr -> upcast usr.["PasswordHash"].Eq(passwordHash)))
|
.Filter(ReqlFunction1(fun usr -> upcast usr.["PasswordHash"].Eq(passwordHash)))
|
||||||
.RunResultAsync<User>(this)
|
.RunResultAsync<User list>(this)
|
||||||
|> Async.AwaitTask
|
return user |> List.tryHead
|
||||||
return match box user with null -> None | _ -> Some user
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set up the environment for MyPrayerJournal
|
/// Set up the environment for MyPrayerJournal
|
||||||
member this.EstablishEnvironment () =
|
member this.EstablishEnvironment (cfg : AppConfig) =
|
||||||
/// Shorthand for the database
|
/// Shorthand for the database
|
||||||
let db () = r.Db("MyPrayerJournal")
|
let db () = r.Db("MyPrayerJournal")
|
||||||
/// Log a step in the database environment set up
|
// Be chatty about what we're doing
|
||||||
let logStep step = sprintf "[MyPrayerJournal] %s" step |> Console.WriteLine
|
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
|
/// Ensure the database exists
|
||||||
let checkDatabase () =
|
let checkDatabase () =
|
||||||
async {
|
async {
|
||||||
logStep "|> Checking database"
|
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
|
match dbList |> List.contains "MyPrayerJournal" with
|
||||||
| true -> ()
|
| true -> ()
|
||||||
| _ -> logStep " Database not found - creating..."
|
| _ -> logStepStart " Database not found - creating..."
|
||||||
do! r.DbCreate("MyPrayerJournal").RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
|
do! r.DbCreate("MyPrayerJournal").RunResultAsync(this)
|
||||||
logStep " ...done"
|
logStepEnd ()
|
||||||
}
|
}
|
||||||
/// Ensure all tables exit
|
/// Ensure all tables exit
|
||||||
let checkTables () =
|
let checkTables () =
|
||||||
async {
|
async {
|
||||||
logStep "|> Checking tables"
|
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 ]
|
[ DataTable.Request; DataTable.User ]
|
||||||
|> List.filter (fun tbl -> not (tables |> List.contains tbl))
|
|> List.filter (fun tbl -> not (tables |> List.contains tbl))
|
||||||
|> List.map (fun tbl ->
|
|> List.map (fun tbl ->
|
||||||
async {
|
async {
|
||||||
logStep <| sprintf " %s table not found - creating..." tbl
|
logStepStart <| sprintf " %s table not found - creating..." tbl
|
||||||
do! db().TableCreate(tbl).RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
|
do! db().TableCreate(tbl).RunResultAsync(this)
|
||||||
logStep " ...done"
|
logStepEnd()
|
||||||
})
|
})
|
||||||
|> List.iter Async.RunSynchronously
|
|> 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
|
/// Ensure the proper indexes exist
|
||||||
let checkIndexes () =
|
let checkIndexes () =
|
||||||
async {
|
async {
|
||||||
logStep "|> Checking indexes"
|
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
|
match reqIdx |> List.contains "UserId" with
|
||||||
| true -> ()
|
| true -> ()
|
||||||
| _ -> logStep <| sprintf " %s.UserId index not found - creating..." DataTable.Request
|
| _ -> logStepStart <| sprintf " %s.UserId index not found - creating..." DataTable.Request
|
||||||
do! db().Table(DataTable.Request).IndexCreate("UserId").RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
|
do! db().Table(DataTable.Request).IndexCreate("UserId").RunResultAsync(this)
|
||||||
logStep " ...done"
|
logStepEnd ()
|
||||||
let! usrIdx = db().Table(DataTable.User).IndexList().RunResultAsync<string list>(this) |> Async.AwaitTask
|
let! usrIdx = db().Table(DataTable.User).IndexList().RunResultAsync<string list>(this)
|
||||||
match usrIdx |> List.contains "Email" with
|
match usrIdx |> List.contains "Email" with
|
||||||
| true -> ()
|
| true -> ()
|
||||||
| _ -> logStep <| sprintf " %s.Email index not found - creating..." DataTable.User
|
| _ -> logStepStart <| sprintf " %s.Email index not found - creating..." DataTable.User
|
||||||
do! db().Table(DataTable.User).IndexCreate("Email").RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
|
do! db().Table(DataTable.User).IndexCreate("Email").RunResultAsync(this)
|
||||||
logStep " ...done"
|
logStepEnd ()
|
||||||
}
|
}
|
||||||
async {
|
async {
|
||||||
logStep "Database checks starting"
|
logStep "Database checks starting"
|
@ -1,6 +1,7 @@
|
|||||||
namespace MyPrayerJournal
|
namespace MyPrayerJournal
|
||||||
|
|
||||||
open Newtonsoft.Json
|
open Newtonsoft.Json
|
||||||
|
open System.Security.Cryptography
|
||||||
|
|
||||||
/// A user
|
/// A user
|
||||||
type User = {
|
type User = {
|
||||||
@ -27,6 +28,11 @@ type User = {
|
|||||||
Name = ""
|
Name = ""
|
||||||
TimeZone = ""
|
TimeZone = ""
|
||||||
LastSeenOn = int64 0 }
|
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
|
/// Request history entry
|
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": {
|
"userSecretsId": "aspnet-WebApplication-0799fe3e-6eaf-4c5f-b40e-7c6bfd5dfa9a",
|
||||||
"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
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNetCore.Hosting": "1.0.0",
|
"Microsoft.NETCore.App": {
|
||||||
"Microsoft.AspNetCore.Owin": "1.0.0",
|
"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.Server.Kestrel": "1.0.1",
|
||||||
"Nancy": "2.0.0-barneyrubble",
|
"Microsoft.AspNetCore.Session": "1.0.0",
|
||||||
"Nancy.Authentication.Forms": "2.0.0-barneyrubble",
|
"Microsoft.AspNetCore.StaticFiles": "1.0.0",
|
||||||
"Nancy.Session.Persistable": "0.9.1-pre",
|
"Microsoft.Extensions.Configuration.CommandLine": "1.0.0",
|
||||||
"Nancy.Session.RethinkDB": "0.9.1-pre",
|
"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",
|
"Newtonsoft.Json": "9.0.1",
|
||||||
"NodaTime": "2.0.0-alpha20160729",
|
"NodaTime": "2.0.0-alpha20160729",
|
||||||
"RethinkDb.Driver": "2.3.15"
|
"RethinkDb.Driver": "2.3.15"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
|
||||||
"netcoreapp1.0": {
|
"tools": {
|
||||||
"dependencies": {
|
"dotnet-compile-fsc":"1.0.0-preview2-*",
|
||||||
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160831",
|
"Microsoft.AspNetCore.Razor.Tools": {
|
||||||
"Microsoft.NETCore.App": {
|
"version": "1.0.0-preview2-final",
|
||||||
"type": "platform",
|
"imports": "portable-net45+win8+dnxcore50"
|
||||||
"version": "1.0.1"
|
},
|
||||||
}
|
"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