Data environment
Created infrastructure for data environment and security configuration
This commit is contained in:
parent
5c1287d8a8
commit
6bd90c854d
20
src/MyPrayerJournal/App.fs
Normal file
20
src/MyPrayerJournal/App.fs
Normal 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
|
90
src/MyPrayerJournal/Config.fs
Normal file
90
src/MyPrayerJournal/Config.fs
Normal 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 }
|
||||
|
78
src/MyPrayerJournal/Data.fs
Normal file
78
src/MyPrayerJournal/Data.fs
Normal 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"
|
||||
}
|
66
src/MyPrayerJournal/Entities.fs
Normal file
66
src/MyPrayerJournal/Entities.fs
Normal 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
|
||||
|
17
src/MyPrayerJournal/config.json
Normal file
17
src/MyPrayerJournal/config.json
Normal 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"
|
||||
}
|
||||
}
|
39
src/MyPrayerJournal/project.json
Normal file
39
src/MyPrayerJournal/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user