Data environment

Created infrastructure for data environment and security configuration
This commit is contained in:
Daniel J. Summers 2016-09-22 21:43:39 -05:00
parent 5c1287d8a8
commit 6bd90c854d
7 changed files with 313 additions and 1 deletions

View File

@ -1 +1,3 @@
# myPrayerJournal
WIP

View File

@ -0,0 +1,20 @@
module App
open MyPrayerJournal
open Nancy
open Nancy.Owin
open Suave.Web
open Suave.Owin
open System
/// Establish the configuration
let cfg = AppConfig.FromJson (System.IO.File.ReadAllText "config.json")
do
cfg.DataConfig.Conn.EstablishEnvironment () |> Async.RunSynchronously
[<EntryPoint>]
let main argv =
let app = OwinApp.ofMidFunc "/" (NancyMiddleware.UseNancy(NancyOptions()))
startWebServer defaultConfig app
0 // return an integer exit code

View File

@ -0,0 +1,90 @@
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 }

View File

@ -0,0 +1,78 @@
[<AutoOpen>]
module Data
open Newtonsoft.Json
open RethinkDb.Driver
open RethinkDb.Driver.Net
open System
let private r = RethinkDB.R
/// Tables for data storage
module DataTable =
/// The table for prayer requests
[<Literal>]
let Request = "Request"
/// The table for users
[<Literal>]
let User = "User"
/// Extensions for the RethinkDB connection
type IConnection with
/// Set up the environment for MyPrayerJournal
member this.EstablishEnvironment () =
/// 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
/// Ensure the database exists
let checkDatabase () =
async {
logStep "|> Checking database..."
let! dbList = r.DbList().RunResultAsync<string list>(this) |> Async.AwaitTask
match dbList |> List.contains "MyPrayerJournal" with
| true -> ()
| _ -> logStep " Database not found - creating..."
do! r.DbCreate("MyPrayerJournal").RunResultAsync(this) |> Async.AwaitTask |> Async.Ignore
logStep " ...done"
}
/// Ensure all tables exit
let checkTables () =
async {
logStep "|> Checking tables..."
let! tables = db().TableList().RunResultAsync<string list>(this) |> Async.AwaitTask
[ 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"
})
|> List.iter Async.RunSynchronously
}
/// Ensure the proper indexes exist
let checkIndexes () =
async {
logStep "|> Checking indexes..."
let! reqIdx = db().Table(DataTable.Request).IndexList().RunResultAsync<string list>(this) |> Async.AwaitTask
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
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"
}
async {
logStep "Database checks starting"
do! checkDatabase ()
do! checkTables ()
do! checkIndexes ()
logStep "Database checks complete"
}

View File

@ -0,0 +1,66 @@
namespace MyPrayerJournal
open Newtonsoft.Json
/// A user
type User = {
/// The Id of the user
[<JsonProperty("id")>]
Id : string
/// The user's e-mail address
Email : string
/// A hash of the user's password
PasswordHash : string
/// The user's name
Name : string
/// The last time the user logged on
LastSeenOn : int64
}
/// Request history entry
type History = {
/// The instant at which the update was made
AsOf : int64
/// The action that was taken on the request
Action : string list
/// The status of the request (filled if it changed)
Status : string option
/// The text of the request (filled if it changed)
Text : string option
}
/// A prayer request
type Request = {
/// The Id of the request
[<JsonProperty("id")>]
Id : string
/// The Id of the user to whom this request belongs
UserId : string
/// The instant this request was entered
EnteredOn : int64
/// The history for this request
History : History list
}
with
/// The current status of the prayer request
member this.Status =
this.History
|> List.sortBy (fun item -> -item.AsOf)
|> List.map (fun item -> item.Status)
|> List.filter Option.isSome
|> List.map Option.get
|> List.head
/// The current text of the prayer request
member this.Text =
this.History
|> List.sortBy (fun item -> -item.AsOf)
|> List.map (fun item -> item.Text)
|> List.filter Option.isSome
|> List.map Option.get
|> List.head
member this.LastActionOn =
this.History
|> List.sortBy (fun item -> -item.AsOf)
|> List.map (fun item -> item.AsOf)
|> List.head

View File

@ -0,0 +1,17 @@
{
// 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"
}
}

View File

@ -0,0 +1,39 @@
{
"version": "1.0.0-*",
"buildOptions": {
"debugType": "portable",
"emitEntryPoint": true,
"compilerName": "fsc",
"compile": {
"includeFiles": [
"Entities.fs",
"Config.fs",
"Data.fs",
"App.fs"
]
}
},
"dependencies": {
"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",
"Newtonsoft.Json": "9.0.1",
"RethinkDb.Driver": "2.3.15",
"Suave": "2.0.0-alpha5"
},
"tools": {
"dotnet-compile-fsc":"1.0.0-preview2-*"
},
"frameworks": {
"netcoreapp1.0": {
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.1"
},
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-160831"
}
}
}
}