()
+ let data = ctx.Data
+ let! tryUser = data.WebLogUser.FindByEmail model.EmailAddress ctx.WebLog.Id
+ match! verifyPassword tryUser model.Password ctx with
+ | Ok _ ->
+ let user = tryUser.Value
let claims = seq {
Claim (ClaimTypes.NameIdentifier, WebLogUserId.toString user.Id)
Claim (ClaimTypes.Name, $"{user.FirstName} {user.LastName}")
@@ -59,8 +74,8 @@ let doLogOn : HttpHandler = fun next ctx -> task {
match model.ReturnTo with
| Some url -> redirectTo false url next ctx
| None -> redirectToGet "admin/dashboard" next ctx
- | _ ->
- do! addMessage ctx { UserMessage.error with Message = "Log on attempt unsuccessful" }
+ | Error msg ->
+ do! addMessage ctx { UserMessage.error with Message = msg }
return! logOn model.ReturnTo next ctx
}
@@ -147,7 +162,9 @@ let private showMyInfo (model : EditMyInfoModel) (user : WebLogUser) : HttpHandl
|> addToHash ViewContext.Model model
|> addToHash "access_level" (AccessLevel.toString user.AccessLevel)
|> addToHash "created_on" (WebLog.localTime ctx.WebLog user.CreatedOn)
- |> addToHash "last_seen_on" (WebLog.localTime ctx.WebLog (defaultArg user.LastSeenOn DateTime.UnixEpoch))
+ |> addToHash "last_seen_on" (WebLog.localTime ctx.WebLog
+ (defaultArg user.LastSeenOn (Instant.FromUnixTimeSeconds 0)))
+
|> adminView "my-info" next ctx
@@ -164,19 +181,13 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
let data = ctx.Data
match! data.WebLogUser.FindById ctx.UserId ctx.WebLog.Id with
| Some user when model.NewPassword = model.NewPasswordConfirm ->
- let pw, salt =
- if model.NewPassword = "" then
- user.PasswordHash, user.Salt
- else
- let newSalt = Guid.NewGuid ()
- hashedPassword model.NewPassword user.Email newSalt, newSalt
+ let pw = if model.NewPassword = "" then user.PasswordHash else createPasswordHash user model.NewPassword
let user =
{ user with
FirstName = model.FirstName
LastName = model.LastName
PreferredName = model.PreferredName
PasswordHash = pw
- Salt = salt
}
do! data.WebLogUser.Update user
let pwMsg = if model.NewPassword = "" then "" else " and updated your password"
@@ -198,9 +209,9 @@ let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
let tryUser =
if model.IsNew then
{ WebLogUser.empty with
- Id = WebLogUserId.create ()
- WebLogId = ctx.WebLog.Id
- CreatedOn = DateTime.UtcNow
+ Id = WebLogUserId.create ()
+ WebLogId = ctx.WebLog.Id
+ CreatedOn = Noda.now ()
} |> someTask
else data.WebLogUser.FindById (WebLogUserId model.Id) ctx.WebLog.Id
match! tryUser with
@@ -211,9 +222,7 @@ let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
else
let toUpdate =
if model.Password = "" then updatedUser
- else
- let salt = Guid.NewGuid ()
- { updatedUser with PasswordHash = hashedPassword model.Password model.Email salt; Salt = salt }
+ else { updatedUser with PasswordHash = createPasswordHash updatedUser model.Password }
do! (if model.IsNew then data.WebLogUser.Add else data.WebLogUser.Update) toUpdate
do! addMessage ctx
{ UserMessage.success with
@@ -227,4 +236,3 @@ let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
next ctx
| None -> return! Error.notFound next ctx
}
-
diff --git a/src/MyWebLog/Maintenance.fs b/src/MyWebLog/Maintenance.fs
index c620721..544de4f 100644
--- a/src/MyWebLog/Maintenance.fs
+++ b/src/MyWebLog/Maintenance.fs
@@ -4,6 +4,7 @@ open System
open System.IO
open Microsoft.Extensions.DependencyInjection
open MyWebLog.Data
+open NodaTime
/// Create the web log information
let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
@@ -41,22 +42,19 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
}
// Create the admin user
- let salt = Guid.NewGuid ()
- let now = DateTime.UtcNow
-
- do! data.WebLogUser.Add
- { WebLogUser.empty with
- Id = userId
- WebLogId = webLogId
- Email = args[3]
- FirstName = "Admin"
- LastName = "User"
- PreferredName = "Admin"
- PasswordHash = Handlers.User.hashedPassword args[4] args[3] salt
- Salt = salt
- AccessLevel = accessLevel
- CreatedOn = now
- }
+ let now = Noda.now ()
+ let user =
+ { WebLogUser.empty with
+ Id = userId
+ WebLogId = webLogId
+ Email = args[3]
+ FirstName = "Admin"
+ LastName = "User"
+ PreferredName = "Admin"
+ AccessLevel = accessLevel
+ CreatedOn = now
+ }
+ do! data.WebLogUser.Add { user with PasswordHash = Handlers.User.createPasswordHash user args[4] }
// Create the default home page
do! data.Page.Add
@@ -70,8 +68,8 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task {
UpdatedOn = now
Text = "This is your default home page.
"
Revisions = [
- { AsOf = now
- Text = Html "This is your default home page.
"
+ { AsOf = now
+ Text = Html "This is your default home page.
"
}
]
}
@@ -155,7 +153,6 @@ let loadTheme (args : string[]) (sp : IServiceProvider) = task {
/// Back up a web log's data
module Backup =
- open System.Threading.Tasks
open MyWebLog.Converters
open Newtonsoft.Json
@@ -165,7 +162,7 @@ module Backup =
Id : ThemeAssetId
/// The updated date for this asset
- UpdatedOn : DateTime
+ UpdatedOn : Instant
/// The data for this asset, base-64 encoded
Data : string
@@ -197,7 +194,7 @@ module Backup =
Path : Permalink
/// The date/time this upload was last updated (file time)
- UpdatedOn : DateTime
+ UpdatedOn : Instant
/// The data for the upload, base-64 encoded
Data : string
@@ -251,10 +248,9 @@ module Backup =
Uploads : EncodedUpload list
}
- /// Create a JSON serializer (uses RethinkDB data implementation's JSON converters)
+ /// Create a JSON serializer
let private getSerializer prettyOutput =
- let serializer = JsonSerializer.CreateDefault ()
- Json.all () |> Seq.iter serializer.Converters.Add
+ let serializer = Json.configure (JsonSerializer.CreateDefault ())
if prettyOutput then serializer.Formatting <- Formatting.Indented
serializer
@@ -382,7 +378,8 @@ module Backup =
printfn ""
printfn "- Importing theme..."
do! data.Theme.Save restore.Theme
- let! _ = restore.Assets |> List.map (EncodedAsset.toAsset >> data.ThemeAsset.Save) |> Task.WhenAll
+ restore.Assets
+ |> List.iter (EncodedAsset.toAsset >> data.ThemeAsset.Save >> Async.AwaitTask >> Async.RunSynchronously)
// Restore web log data
@@ -393,19 +390,20 @@ module Backup =
do! data.WebLogUser.Restore restore.Users
printfn "- Restoring categories and tag mappings..."
- do! data.TagMap.Restore restore.TagMappings
- do! data.Category.Restore restore.Categories
+ if not (List.isEmpty restore.TagMappings) then do! data.TagMap.Restore restore.TagMappings
+ if not (List.isEmpty restore.Categories) then do! data.Category.Restore restore.Categories
printfn "- Restoring pages..."
- do! data.Page.Restore restore.Pages
+ if not (List.isEmpty restore.Pages) then do! data.Page.Restore restore.Pages
printfn "- Restoring posts..."
- do! data.Post.Restore restore.Posts
+ if not (List.isEmpty restore.Posts) then do! data.Post.Restore restore.Posts
// TODO: comments not yet implemented
printfn "- Restoring uploads..."
- do! data.Upload.Restore (restore.Uploads |> List.map EncodedUpload.toUpload)
+ if not (List.isEmpty restore.Uploads) then
+ do! data.Upload.Restore (restore.Uploads |> List.map EncodedUpload.toUpload)
displayStats "Restored for <>NAME<>:" restore.WebLog restore
}
@@ -490,3 +488,22 @@ let upgradeUser (args : string[]) (sp : IServiceProvider) = task {
| 3 -> do! doUserUpgrade args[1] args[2] (sp.GetRequiredService ())
| _ -> eprintfn "Usage: myWebLog upgrade-user [web-log-url-base] [email-address]"
}
+
+/// Set a user's password
+let doSetPassword urlBase email password (data : IData) = task {
+ match! data.WebLog.FindByHost urlBase with
+ | Some webLog ->
+ match! data.WebLogUser.FindByEmail email webLog.Id with
+ | Some user ->
+ do! data.WebLogUser.Update { user with PasswordHash = Handlers.User.createPasswordHash user password }
+ printfn $"Password for user {email} at {webLog.Name} set successfully"
+ | None -> eprintfn $"ERROR: no user {email} found at {urlBase}"
+ | None -> eprintfn $"ERROR: no web log found for {urlBase}"
+}
+
+/// Set a user's password if the command-line arguments are good
+let setPassword (args : string[]) (sp : IServiceProvider) = task {
+ match args.Length with
+ | 4 -> do! doSetPassword args[1] args[2] args[3] (sp.GetRequiredService ())
+ | _ -> eprintfn "Usage: myWebLog set-password [web-log-url-base] [email-address] [password]"
+}
diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs
index 5eca40c..a9fecf4 100644
--- a/src/MyWebLog/Program.fs
+++ b/src/MyWebLog/Program.fs
@@ -29,11 +29,14 @@ type WebLogMiddleware (next : RequestDelegate, log : ILogger)
open System
open Microsoft.Extensions.DependencyInjection
open MyWebLog.Data
+open Newtonsoft.Json
+open Npgsql
/// Logic to obtain a data connection and implementation based on configured values
module DataImplementation =
open MyWebLog.Converters
+ // open Npgsql.Logging
open RethinkDb.Driver.FSharp
open RethinkDb.Driver.Net
@@ -43,23 +46,29 @@ module DataImplementation =
let await it = (Async.AwaitTask >> Async.RunSynchronously) it
let connStr name = config.GetConnectionString name
let hasConnStr name = (connStr >> isNull >> not) name
- let createSQLite connStr =
+ let createSQLite connStr : IData =
let log = sp.GetRequiredService> ()
let conn = new SqliteConnection (connStr)
log.LogInformation $"Using SQLite database {conn.DataSource}"
await (SQLiteData.setUpConnection conn)
- SQLiteData (conn, log)
+ SQLiteData (conn, log, Json.configure (JsonSerializer.CreateDefault ()))
if hasConnStr "SQLite" then
- upcast createSQLite (connStr "SQLite")
+ createSQLite (connStr "SQLite")
elif hasConnStr "RethinkDB" then
- let log = sp.GetRequiredService> ()
- Json.all () |> Seq.iter Converter.Serializer.Converters.Add
+ let log = sp.GetRequiredService> ()
+ let _ = Json.configure Converter.Serializer
let rethinkCfg = DataConfig.FromUri (connStr "RethinkDB")
let conn = await (rethinkCfg.CreateConnectionAsync log)
- upcast RethinkDbData (conn, rethinkCfg, log)
+ RethinkDbData (conn, rethinkCfg, log)
+ elif hasConnStr "PostgreSQL" then
+ let log = sp.GetRequiredService> ()
+ // NpgsqlLogManager.Provider <- ConsoleLoggingProvider NpgsqlLogLevel.Debug
+ let conn = new NpgsqlConnection (connStr "PostgreSQL")
+ log.LogInformation $"Using PostgreSQL database {conn.Host}:{conn.Port}/{conn.Database}"
+ PostgresData (conn, log, Json.configure (JsonSerializer.CreateDefault ()))
else
- upcast createSQLite "Data Source=./myweblog.db;Cache=Shared"
+ createSQLite "Data Source=./myweblog.db;Cache=Shared"
open System.Threading.Tasks
@@ -76,6 +85,7 @@ let showHelp () =
printfn "init Initializes a new web log"
printfn "load-theme Load a theme"
printfn "restore Restore a JSON file backup (prompt before overwriting)"
+ printfn "set-password Set a password for a specific user"
printfn "upgrade-user Upgrade a WebLogAdmin user to a full Administrator"
printfn " "
printfn "For more information on a particular command, run it with no options."
@@ -88,6 +98,7 @@ open Giraffe.EndpointRouting
open Microsoft.AspNetCore.Authentication.Cookies
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.HttpOverrides
+open Microsoft.Extensions.Caching.Distributed
open NeoSmart.Caching.Sqlite
open RethinkDB.DistributedCache
@@ -108,8 +119,9 @@ let rec main args =
let _ = builder.Services.AddAuthorization ()
let _ = builder.Services.AddAntiforgery ()
- let sp = builder.Services.BuildServiceProvider ()
+ let sp = builder.Services.BuildServiceProvider ()
let data = DataImplementation.get sp
+ let _ = builder.Services.AddSingleton data.Serializer
task {
do! data.StartUp ()
@@ -121,23 +133,36 @@ let rec main args =
match data with
| :? RethinkDbData as rethink ->
// A RethinkDB connection is designed to work as a singleton
- builder.Services.AddSingleton data |> ignore
- builder.Services.AddDistributedRethinkDBCache (fun opts ->
- opts.TableName <- "Session"
- opts.Connection <- rethink.Conn)
- |> ignore
+ let _ = builder.Services.AddSingleton data
+ let _ =
+ builder.Services.AddDistributedRethinkDBCache (fun opts ->
+ opts.TableName <- "Session"
+ opts.Connection <- rethink.Conn)
+ ()
| :? SQLiteData as sql ->
// ADO.NET connections are designed to work as per-request instantiation
let cfg = sp.GetRequiredService ()
- builder.Services.AddScoped (fun sp ->
- let conn = new SqliteConnection (sql.Conn.ConnectionString)
- SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously
- conn)
- |> ignore
- builder.Services.AddScoped () |> ignore
+ let _ =
+ builder.Services.AddScoped (fun sp ->
+ let conn = new SqliteConnection (sql.Conn.ConnectionString)
+ SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously
+ conn)
+ let _ = builder.Services.AddScoped () |> ignore
// Use SQLite for caching as well
let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./session.db"
- builder.Services.AddSqliteCache (fun o -> o.CachePath <- cachePath) |> ignore
+ let _ = builder.Services.AddSqliteCache (fun o -> o.CachePath <- cachePath)
+ ()
+ | :? PostgresData ->
+ // ADO.NET connections are designed to work as per-request instantiation
+ let cfg = sp.GetRequiredService ()
+ let _ =
+ builder.Services.AddScoped (fun sp ->
+ new NpgsqlConnection (cfg.GetConnectionString "PostgreSQL"))
+ let _ = builder.Services.AddScoped ()
+ let _ =
+ builder.Services.AddSingleton (fun sp ->
+ Postgres.DistributedCache (cfg.GetConnectionString "PostgreSQL") :> IDistributedCache)
+ ()
| _ -> ()
let _ = builder.Services.AddSession(fun opts ->
@@ -159,6 +184,7 @@ let rec main args =
| Some it when it = "restore" -> Maintenance.Backup.restoreFromBackup args app.Services
| Some it when it = "do-restore" -> Maintenance.Backup.restoreFromBackup args app.Services
| Some it when it = "upgrade-user" -> Maintenance.upgradeUser args app.Services
+ | Some it when it = "set-password" -> Maintenance.setPassword args app.Services
| Some it when it = "help" -> showHelp ()
| Some it ->
printfn $"""Unrecognized command "{it}" - valid commands are:"""
diff --git a/src/MyWebLog/appsettings.json b/src/MyWebLog/appsettings.json
index 6c0b98c..62fa309 100644
--- a/src/MyWebLog/appsettings.json
+++ b/src/MyWebLog/appsettings.json
@@ -1,5 +1,5 @@
{
- "Generator": "myWebLog 2.0-rc1",
+ "Generator": "myWebLog 2.0-rc2",
"Logging": {
"LogLevel": {
"MyWebLog.Handlers": "Information"
diff --git a/src/admin-theme/version.txt b/src/admin-theme/version.txt
index 18c98a2..80104df 100644
--- a/src/admin-theme/version.txt
+++ b/src/admin-theme/version.txt
@@ -1,2 +1,2 @@
myWebLog Admin
-2.0.0-rc1
\ No newline at end of file
+2.0.0-rc2
\ No newline at end of file
diff --git a/src/default-theme/version.txt b/src/default-theme/version.txt
index 74f4501..9757c99 100644
--- a/src/default-theme/version.txt
+++ b/src/default-theme/version.txt
@@ -1,2 +1,2 @@
myWebLog Default Theme
-2.0.0-rc1
\ No newline at end of file
+2.0.0-rc2
\ No newline at end of file