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