Reorg repo, add new Vue 3 app

This commit is contained in:
2021-09-19 20:52:21 -04:00
parent c1f6054e15
commit ea29f0572f
28 changed files with 30580 additions and 7 deletions

View File

@@ -0,0 +1,186 @@
namespace MyPrayerJournal
open System
open System.Collections.Generic
/// JSON converters for various DUs
module Converters =
open Microsoft.FSharpLu.Json
open Newtonsoft.Json
/// JSON converter for request IDs
type RequestIdJsonConverter () =
inherit JsonConverter<RequestId> ()
override __.WriteJson(writer : JsonWriter, value : RequestId, _ : JsonSerializer) =
(RequestId.toString >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : RequestId, _ : bool, _ : JsonSerializer) =
(string >> RequestId.fromIdString) reader.Value
/// JSON converter for user IDs
type UserIdJsonConverter () =
inherit JsonConverter<UserId> ()
override __.WriteJson(writer : JsonWriter, value : UserId, _ : JsonSerializer) =
(UserId.toString >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : UserId, _ : bool, _ : JsonSerializer) =
(string >> UserId) reader.Value
/// JSON converter for Ticks
type TicksJsonConverter () =
inherit JsonConverter<Ticks> ()
override __.WriteJson(writer : JsonWriter, value : Ticks, _ : JsonSerializer) =
(Ticks.toLong >> writer.WriteValue) value
override __.ReadJson(reader: JsonReader, _ : Type, _ : Ticks, _ : bool, _ : JsonSerializer) =
(string >> int64 >> Ticks) reader.Value
/// A sequence of all custom converters needed for myPrayerJournal
let all : JsonConverter seq =
seq {
yield RequestIdJsonConverter ()
yield UserIdJsonConverter ()
yield TicksJsonConverter ()
yield CompactUnionJsonConverter true
}
/// RavenDB index declarations
module Indexes =
open Raven.Client.Documents.Indexes
/// Index requests for a journal view
// fsharplint:disable-next-line TypeNames
type Requests_AsJournal () as this =
inherit AbstractJavaScriptIndexCreationTask ()
do
this.Maps <- HashSet<string> [
"""docs.Requests.Select(req => new {
requestId = req.Id.Replace("Requests/", ""),
userId = req.userId,
text = req.history.Where(hist => hist.text != null).OrderByDescending(hist => hist.asOf).First().text,
asOf = req.history.OrderByDescending(hist => hist.asOf).First().asOf,
lastStatus = req.history.OrderByDescending(hist => hist.asOf).First().status,
snoozedUntil = req.snoozedUntil,
showAfter = req.showAfter,
recurType = req.recurType,
recurCount = req.recurCount
})"""
]
this.Fields <-
[ "requestId", IndexFieldOptions (Storage = Nullable FieldStorage.Yes)
"text", IndexFieldOptions (Storage = Nullable FieldStorage.Yes)
"asOf", IndexFieldOptions (Storage = Nullable FieldStorage.Yes)
"lastStatus", IndexFieldOptions (Storage = Nullable FieldStorage.Yes)
]
|> dict
|> Dictionary<string, IndexFieldOptions>
/// All data manipulations within myPrayerJournal
module Data =
open Indexes
open Microsoft.FSharpLu
open Raven.Client.Documents
open Raven.Client.Documents.Linq
open Raven.Client.Documents.Session
/// Add a history entry
let addHistory reqId (hist : History) (sess : IAsyncDocumentSession) =
sess.Advanced.Patch<Request, History> (
RequestId.toString reqId,
(fun r -> r.history :> IEnumerable<History>),
fun (h : JavaScriptArray<History>) -> h.Add (hist) :> obj)
/// Add a note
let addNote reqId (note : Note) (sess : IAsyncDocumentSession) =
sess.Advanced.Patch<Request, Note> (
RequestId.toString reqId,
(fun r -> r.notes :> IEnumerable<Note>),
fun (h : JavaScriptArray<Note>) -> h.Add (note) :> obj)
/// Add a request
let addRequest req (sess : IAsyncDocumentSession) =
sess.StoreAsync (req, req.Id)
/// Retrieve all answered requests for the given user
let answeredRequests userId (sess : IAsyncDocumentSession) =
task {
let! reqs =
sess.Query<JournalRequest, Requests_AsJournal>()
.Where(fun r -> r.userId = userId && r.lastStatus = "Answered")
.OrderByDescending(fun r -> r.asOf)
.ProjectInto<JournalRequest>()
.ToListAsync ()
return List.ofSeq reqs
}
/// Retrieve the user's current journal
let journalByUserId userId (sess : IAsyncDocumentSession) =
task {
let! jrnl =
sess.Query<JournalRequest, Requests_AsJournal>()
.Where(fun r -> r.userId = userId && r.lastStatus <> "Answered")
.OrderBy(fun r -> r.asOf)
.ProjectInto<JournalRequest>()
.ToListAsync()
return
jrnl
|> Seq.map (fun r -> r.history <- []; r.notes <- []; r)
|> List.ofSeq
}
/// Save changes in the current document session
let saveChanges (sess : IAsyncDocumentSession) =
sess.SaveChangesAsync ()
/// Retrieve a request, including its history and notes, by its ID and user ID
let tryFullRequestById reqId userId (sess : IAsyncDocumentSession) =
task {
let! req = RequestId.toString reqId |> sess.LoadAsync
return match Option.fromObject req with Some r when r.userId = userId -> Some r | _ -> None
}
/// Retrieve a request by its ID and user ID (without notes and history)
let tryRequestById reqId userId (sess : IAsyncDocumentSession) =
task {
match! tryFullRequestById reqId userId sess with
| Some r -> return Some { r with history = []; notes = [] }
| _ -> return None
}
/// Retrieve notes for a request by its ID and user ID
let notesById reqId userId (sess : IAsyncDocumentSession) =
task {
match! tryFullRequestById reqId userId sess with
| Some req -> return req.notes
| None -> return []
}
/// Retrieve a journal request by its ID and user ID
let tryJournalById reqId userId (sess : IAsyncDocumentSession) =
task {
let! req =
sess.Query<Request, Requests_AsJournal>()
.Where(fun x -> x.Id = (RequestId.toString reqId) && x.userId = userId)
.ProjectInto<JournalRequest>()
.FirstOrDefaultAsync ()
return
Option.fromObject req
|> Option.map (fun r -> r.history <- []; r.notes <- []; r)
}
/// Update the recurrence for a request
let updateRecurrence reqId recurType recurCount (sess : IAsyncDocumentSession) =
sess.Advanced.Patch<Request, Recurrence> (RequestId.toString reqId, (fun r -> r.recurType), recurType)
sess.Advanced.Patch<Request, int16> (RequestId.toString reqId, (fun r -> r.recurCount), recurCount)
/// Update a snoozed request
let updateSnoozed reqId until (sess : IAsyncDocumentSession) =
sess.Advanced.Patch<Request, Ticks> (RequestId.toString reqId, (fun r -> r.snoozedUntil), until)
sess.Advanced.Patch<Request, Ticks> (RequestId.toString reqId, (fun r -> r.showAfter), until)
/// Update the "show after" timestamp for a request
let updateShowAfter reqId showAfter (sess : IAsyncDocumentSession) =
sess.Advanced.Patch<Request, Ticks> (RequestId.toString reqId, (fun r -> r.showAfter), showAfter)

View File

@@ -0,0 +1,171 @@
[<AutoOpen>]
/// The data model for myPrayerJournal
module MyPrayerJournal.Domain
// fsharplint:disable RecordFieldNames
open Cuid
/// Request ID is a CUID
type RequestId =
| RequestId of Cuid
module RequestId =
/// The string representation of the request ID
let toString = function RequestId x -> $"Requests/{Cuid.toString x}"
/// Create a request ID from a string representation
let fromIdString (x : string) = x.Replace ("Requests/", "") |> (Cuid >> RequestId)
/// User ID is a string (the "sub" part of the JWT)
type UserId =
| UserId of string
module UserId =
/// The string representation of the user ID
let toString = function UserId x -> x
/// A long integer representing seconds since the epoch
type Ticks =
| Ticks of int64
module Ticks =
/// The int64 (long) representation of ticks
let toLong = function Ticks x -> x
/// How frequently a request should reappear after it is marked "Prayed"
type Recurrence =
| Immediate
| Hours
| Days
| Weeks
module Recurrence =
/// Create a recurrence value from a string
let fromString =
function
| "Immediate" -> Immediate
| "Hours" -> Hours
| "Days" -> Days
| "Weeks" -> Weeks
| it -> invalidOp $"{it} is not a valid recurrence"
/// The duration of the recurrence
let duration =
function
| Immediate -> 0L
| Hours -> 3600000L
| Days -> 86400000L
| Weeks -> 604800000L
/// The action taken on a request as part of a history entry
type RequestAction =
| Created
| Prayed
| Updated
| Answered
module RequestAction =
/// Create a RequestAction from a string
let fromString =
function
| "Created" -> Created
| "Prayed" -> Prayed
| "Updated" -> Updated
| "Answered" -> Answered
| it -> invalidOp $"Bad request action {it}"
/// History is a record of action taken on a prayer request, including updates to its text
[<CLIMutable; NoComparison; NoEquality>]
type History =
{ /// The time when this history entry was made
asOf : Ticks
/// The status for this history entry
status : RequestAction
/// The text of the update, if applicable
text : string option
}
with
/// An empty history entry
static member empty =
{ asOf = Ticks 0L
status = Created
text = None
}
/// Note is a note regarding a prayer request that does not result in an update to its text
[<CLIMutable; NoComparison; NoEquality>]
type Note =
{ /// The time when this note was made
asOf : Ticks
/// The text of the notes
notes : string
}
with
/// An empty note
static member empty =
{ asOf = Ticks 0L
notes = ""
}
/// Request is the identifying record for a prayer request
[<CLIMutable; NoComparison; NoEquality>]
type Request =
{ /// The ID of the request
Id : string
/// The time this request was initially entered
enteredOn : Ticks
/// The ID of the user to whom this request belongs ("sub" from the JWT)
userId : UserId
/// The time at which this request should reappear in the user's journal by manual user choice
snoozedUntil : Ticks
/// The time at which this request should reappear in the user's journal by recurrence
showAfter : Ticks
/// The type of recurrence for this request
recurType : Recurrence
/// How many of the recurrence intervals should occur between appearances in the journal
recurCount : int16
/// The history entries for this request
history : History list
/// The notes for this request
notes : Note list
}
with
/// An empty request
static member empty =
{ Id = ""
enteredOn = Ticks 0L
userId = UserId ""
snoozedUntil = Ticks 0L
showAfter = Ticks 0L
recurType = Immediate
recurCount = 0s
history = []
notes = []
}
/// JournalRequest is the form of a prayer request returned for the request journal display. It also contains
/// properties that may be filled for history and notes.
// RavenDB doesn't like the "@"-suffixed properties from record types in a ProjectInto clause
[<NoComparison; NoEquality>]
type JournalRequest () =
/// The ID of the request (just the CUID part)
[<DefaultValue>] val mutable requestId : string
/// The ID of the user to whom the request belongs
[<DefaultValue>] val mutable userId : UserId
/// The current text of the request
[<DefaultValue>] val mutable text : string
/// The last time action was taken on the request
[<DefaultValue>] val mutable asOf : Ticks
/// The last status for the request
[<DefaultValue>] val mutable lastStatus : string
/// The time that this request should reappear in the user's journal
[<DefaultValue>] val mutable snoozedUntil : Ticks
/// The time after which this request should reappear in the user's journal by configured recurrence
[<DefaultValue>] val mutable showAfter : Ticks
/// The type of recurrence for this request
[<DefaultValue>] val mutable recurType : Recurrence
/// How many of the recurrence intervals should occur between appearances in the journal
[<DefaultValue>] val mutable recurCount : int16
/// History entries for the request
[<DefaultValue>] val mutable history : History list
/// Note entries for the request
[<DefaultValue>] val mutable notes : Note list

View File

@@ -0,0 +1,376 @@
/// HTTP handlers for the myPrayerJournal API
[<RequireQualifiedAccess>]
module MyPrayerJournal.Handlers
// fsharplint:disable RecordFieldNames
open Giraffe
/// Handler to return Vue files
module Vue =
/// The application index page
let app : HttpHandler = htmlFile "wwwroot/index.html"
open System
/// Handlers for error conditions
module Error =
open Microsoft.Extensions.Logging
/// Handle errors
let error (ex : Exception) (log : ILogger) =
log.LogError (EventId(), ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> json ex.Message
/// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there
let notFound : HttpHandler =
fun next ctx ->
[ "/journal"; "/legal"; "/request"; "/user" ]
|> List.filter ctx.Request.Path.Value.StartsWith
|> List.length
|> function
| 0 -> (setStatusCode 404 >=> json ([ "error", "not found" ] |> dict)) next ctx
| _ -> Vue.app next ctx
open Cuid
/// Handler helpers
[<AutoOpen>]
module private Helpers =
open Microsoft.AspNetCore.Http
open Raven.Client.Documents
open System.Threading.Tasks
open System.Security.Claims
/// Create a RavenDB session
let session (ctx : HttpContext) =
let sess = ctx.GetService<IDocumentStore>().OpenAsyncSession ()
sess.Advanced.WaitForIndexesAfterSaveChanges ()
sess
/// Get the user's "sub" claim
let user (ctx : HttpContext) =
ctx.User.Claims |> Seq.tryFind (fun u -> u.Type = ClaimTypes.NameIdentifier)
/// Get the current user's ID
// NOTE: this may raise if you don't run the request through the authorize handler first
let userId ctx =
((user >> Option.get) ctx).Value |> UserId
/// Create a request ID from a string
let toReqId x =
match Cuid.ofString x with
| Ok cuid -> cuid
| Error msg -> invalidOp msg
|> RequestId
/// Return a 201 CREATED response
let created next ctx =
setStatusCode 201 next ctx
/// The "now" time in JavaScript as Ticks
let jsNow () =
(int64 >> (*) 1000L >> Ticks) <| DateTime.UtcNow.Subtract(DateTime (1970, 1, 1, 0, 0, 0)).TotalSeconds
/// Handler to return a 403 Not Authorized reponse
let notAuthorized : HttpHandler =
setStatusCode 403 >=> fun _ _ -> Task.FromResult<HttpContext option> None
/// Handler to require authorization
let authorize : HttpHandler =
fun next ctx -> match user ctx with Some _ -> next ctx | None -> notAuthorized next ctx
/// Flip JSON result so we can pipe into it
let asJson<'T> next ctx (o : 'T) =
json o next ctx
/// Work-around to let the Json.NET serializer synchronously deserialize from the request stream
// TODO: Remove this once there is an async serializer
// let allowSyncIO : HttpHandler =
// fun next ctx ->
// match ctx.Features.Get<Features.IHttpBodyControlFeature>() with
// | null -> ()
// | f -> f.AllowSynchronousIO <- true
// next ctx
/// Strongly-typed models for post requests
module Models =
/// A history entry addition (AKA request update)
[<CLIMutable>]
type HistoryEntry =
{ /// The status of the history update
status : string
/// The text of the update
updateText : string
}
/// An additional note
[<CLIMutable>]
type NoteEntry =
{ /// The notes being added
notes : string
}
/// Recurrence update
[<CLIMutable>]
type Recurrence =
{ /// The recurrence type
recurType : string
/// The recurrence cound
recurCount : int16
}
/// A prayer request
[<CLIMutable>]
type Request =
{ /// The text of the request
requestText : string
/// The recurrence type
recurType : string
/// The recurrence count
recurCount : int16
}
/// The time until which a request should not appear in the journal
[<CLIMutable>]
type SnoozeUntil =
{ /// The time at which the request should reappear
until : int64
}
/// /api/journal URLs
module Journal =
/// GET /api/journal
let journal : HttpHandler =
authorize
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
let! jrnl = Data.journalByUserId usrId sess
return! json jrnl next ctx
}
/// /api/request URLs
module Request =
/// POST /api/request
let add : HttpHandler =
authorize
// >=> allowSyncIO
>=> fun next ctx ->
task {
let! r = ctx.BindJsonAsync<Models.Request> ()
use sess = session ctx
let reqId = (Cuid.generate >> RequestId) ()
let usrId = userId ctx
let now = jsNow ()
do! Data.addRequest
{ Request.empty with
Id = RequestId.toString reqId
userId = usrId
enteredOn = now
showAfter = Ticks 0L
recurType = Recurrence.fromString r.recurType
recurCount = r.recurCount
history = [
{ asOf = now
status = Created
text = Some r.requestText
}
]
} sess
do! Data.saveChanges sess
match! Data.tryJournalById reqId usrId sess with
| Some req -> return! (setStatusCode 201 >=> json req) next ctx
| None -> return! Error.notFound next ctx
}
/// POST /api/request/[req-id]/history
let addHistory requestId : HttpHandler =
authorize
// >=> allowSyncIO
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
let reqId = toReqId requestId
match! Data.tryRequestById reqId usrId sess with
| Some req ->
let! hist = ctx.BindJsonAsync<Models.HistoryEntry> ()
let now = jsNow ()
let act = RequestAction.fromString hist.status
Data.addHistory reqId
{ asOf = now
status = act
text = match hist.updateText with null | "" -> None | x -> Some x
} sess
match act with
| Prayed ->
let nextShow =
match Recurrence.duration req.recurType with
| 0L -> 0L
| duration -> (Ticks.toLong now) + (duration * int64 req.recurCount)
Data.updateShowAfter reqId (Ticks nextShow) sess
| _ -> ()
do! Data.saveChanges sess
return! created next ctx
| None -> return! Error.notFound next ctx
}
/// POST /api/request/[req-id]/note
let addNote requestId : HttpHandler =
authorize
// >=> allowSyncIO
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
let reqId = toReqId requestId
match! Data.tryRequestById reqId usrId sess with
| Some _ ->
let! notes = ctx.BindJsonAsync<Models.NoteEntry> ()
Data.addNote reqId { asOf = jsNow (); notes = notes.notes } sess
do! Data.saveChanges sess
return! created next ctx
| None -> return! Error.notFound next ctx
}
/// GET /api/requests/answered
let answered : HttpHandler =
authorize
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
let! reqs = Data.answeredRequests usrId sess
return! json reqs next ctx
}
/// GET /api/request/[req-id]
let get requestId : HttpHandler =
authorize
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
match! Data.tryJournalById (toReqId requestId) usrId sess with
| Some req -> return! json req next ctx
| None -> return! Error.notFound next ctx
}
/// GET /api/request/[req-id]/full
let getFull requestId : HttpHandler =
authorize
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
match! Data.tryFullRequestById (toReqId requestId) usrId sess with
| Some req -> return! json req next ctx
| None -> return! Error.notFound next ctx
}
/// GET /api/request/[req-id]/notes
let getNotes requestId : HttpHandler =
authorize
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
let! notes = Data.notesById (toReqId requestId) usrId sess
return! json notes next ctx
}
/// PATCH /api/request/[req-id]/show
let show requestId : HttpHandler =
authorize
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
let reqId = toReqId requestId
match! Data.tryRequestById reqId usrId sess with
| Some _ ->
Data.updateShowAfter reqId (Ticks 0L) sess
do! Data.saveChanges sess
return! setStatusCode 204 next ctx
| None -> return! Error.notFound next ctx
}
/// PATCH /api/request/[req-id]/snooze
let snooze requestId : HttpHandler =
authorize
// >=> allowSyncIO
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
let reqId = toReqId requestId
match! Data.tryRequestById reqId usrId sess with
| Some _ ->
let! until = ctx.BindJsonAsync<Models.SnoozeUntil> ()
Data.updateSnoozed reqId (Ticks until.until) sess
do! Data.saveChanges sess
return! setStatusCode 204 next ctx
| None -> return! Error.notFound next ctx
}
/// PATCH /api/request/[req-id]/recurrence
let updateRecurrence requestId : HttpHandler =
authorize
// >=> allowSyncIO
>=> fun next ctx ->
task {
use sess = session ctx
let usrId = userId ctx
let reqId = toReqId requestId
match! Data.tryRequestById reqId usrId sess with
| Some _ ->
let! recur = ctx.BindJsonAsync<Models.Recurrence> ()
let recurrence = Recurrence.fromString recur.recurType
Data.updateRecurrence reqId recurrence recur.recurCount sess
match recurrence with Immediate -> Data.updateShowAfter reqId (Ticks 0L) sess | _ -> ()
do! Data.saveChanges sess
return! setStatusCode 204 next ctx
| None -> return! Error.notFound next ctx
}
open Giraffe.EndpointRouting
/// The routes for myPrayerJournal
let routes =
[ route "/" Vue.app
subRoute "/api/" [
GET [
route "journal" Journal.journal
subRoute "request" [
route "s/answered" Request.answered
routef "/%s/full" Request.getFull
routef "/%s/notes" Request.getNotes
routef "/%s" Request.get
]
]
PATCH [
subRoute "request" [
routef "/%s/recurrence" Request.updateRecurrence
routef "/%s/show" Request.show
routef "/%s/snooze" Request.snooze
]
]
POST [
subRoute "request" [
route "" Request.add
routef "/%s/history" Request.addHistory
routef "/%s/note" Request.addNote
]
]
]
]

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Version>3.0.0.0</Version>
</PropertyGroup>
<ItemGroup>
<Compile Include="Domain.fs" />
<Compile Include="Data.fs" />
<Compile Include="Handlers.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FunctionalCuid" Version="1.0.0" />
<PackageReference Include="Giraffe" Version="5.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="5.0.10" />
<PackageReference Include="Microsoft.FSharpLu" Version="0.11.7" />
<PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.7" />
<PackageReference Include="RavenDb.Client" Version="4.2.102" />
<PackageReference Include="RethinkDb.Driver" Version="2.3.150" />
</ItemGroup>
<ItemGroup>
<Folder Include="wwwroot\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,147 @@
module MyPrayerJournal.Api
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open System.IO
/// Configuration functions for the application
module Configure =
/// Configure the content root
let contentRoot root (bldr : IWebHostBuilder) =
bldr.UseContentRoot root
open Microsoft.Extensions.Configuration
/// Configure the application configuration
let appConfiguration (bldr : IWebHostBuilder) =
let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) =
cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath)
.AddJsonFile("appsettings.json", optional = true, reloadOnChange = true)
.AddJsonFile(sprintf "appsettings.%s.json" ctx.HostingEnvironment.EnvironmentName)
.AddEnvironmentVariables ()
|> ignore
bldr.ConfigureAppConfiguration configuration
open Microsoft.AspNetCore.Server.Kestrel.Core
/// Configure Kestrel from appsettings.json
let kestrel (bldr : IWebHostBuilder) =
let kestrelOpts (ctx : WebHostBuilderContext) (opts : KestrelServerOptions) =
(ctx.Configuration.GetSection >> opts.Configure >> ignore) "Kestrel"
bldr.UseKestrel().ConfigureKestrel kestrelOpts
/// Configure the web root directory
let webRoot pathSegments (bldr : IWebHostBuilder) =
(Path.Combine >> bldr.UseWebRoot) pathSegments
open Giraffe
open Microsoft.AspNetCore.Authentication.JwtBearer
open Microsoft.Extensions.DependencyInjection
open MyPrayerJournal.Indexes
open Newtonsoft.Json
open Newtonsoft.Json.Serialization
open Raven.Client.Documents
open Raven.Client.Documents.Indexes
open System.Security.Cryptography.X509Certificates
/// Configure dependency injection
let services (bldr : IWebHostBuilder) =
let svcs (sc : IServiceCollection) =
/// Custom settings for the JSON serializer (uses compact representation for options and DUs)
let jsonSettings =
let x = NewtonsoftJson.Serializer.DefaultSettings
Converters.all |> List.ofSeq |> List.iter x.Converters.Add
x.NullValueHandling <- NullValueHandling.Ignore
x.MissingMemberHandling <- MissingMemberHandling.Error
x.Formatting <- Formatting.Indented
x.ContractResolver <- DefaultContractResolver ()
x
use sp = sc.BuildServiceProvider ()
let cfg = sp.GetRequiredService<IConfiguration> ()
sc.AddRouting()
.AddGiraffe()
.AddAuthentication(
/// Use HTTP "Bearer" authentication with JWTs
fun opts ->
opts.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme
opts.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(
/// Configure JWT options with Auth0 options from configuration
fun opts ->
let jwtCfg = cfg.GetSection "Auth0"
opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"]
opts.Audience <- jwtCfg.["Id"]
)
|> ignore
sc.AddSingleton<Json.ISerializer> (NewtonsoftJson.Serializer jsonSettings)
|> ignore
let config = sc.BuildServiceProvider().GetRequiredService<IConfiguration>().GetSection "RavenDB"
let store = new DocumentStore ()
store.Urls <- [| config.["URL"] |]
store.Database <- config.["Database"]
match isNull config.["Certificate"] with
| true -> ()
| false -> store.Certificate <- new X509Certificate2 (config.["Certificate"], config.["Password"])
store.Conventions.CustomizeJsonSerializer <- fun x -> Converters.all |> List.ofSeq |> List.iter x.Converters.Add
store.Initialize () |> (sc.AddSingleton >> ignore)
IndexCreation.CreateIndexes (typeof<Requests_AsJournal>.Assembly, store)
bldr.ConfigureServices svcs
open Microsoft.Extensions.Logging
open Microsoft.Extensions.Hosting
/// Configure logging
let logging (bldr : IWebHostBuilder) =
let logz (log : ILoggingBuilder) =
let env = log.Services.BuildServiceProvider().GetService<IWebHostEnvironment> ()
match env.IsDevelopment () with
| true -> log
| false -> log.AddFilter(fun l -> l > LogLevel.Information)
|> function l -> l.AddConsole().AddDebug()
|> ignore
bldr.ConfigureLogging logz
open System
open Giraffe.EndpointRouting
/// Configure the web application
let application (bldr : IWebHostBuilder) =
let appConfig =
Action<IApplicationBuilder> (
fun (app : IApplicationBuilder) ->
let env = app.ApplicationServices.GetService<IWebHostEnvironment> ()
match env.IsDevelopment () with
| true -> app.UseDeveloperExceptionPage ()
| false -> app.UseGiraffeErrorHandler Handlers.Error.error
|> function
| a ->
a.UseAuthentication()
.UseStaticFiles()
.UseRouting()
.UseEndpoints (fun e -> e.MapGiraffeEndpoints Handlers.routes)
|> ignore)
bldr.Configure appConfig
/// Compose all the configurations into one
let webHost appRoot pathSegments =
contentRoot appRoot
>> appConfiguration
>> kestrel
>> webRoot (Array.concat [ [| appRoot |]; pathSegments ])
>> services
>> logging
>> application
/// Build the web host from the given configuration
let buildHost (bldr : IWebHostBuilder) = bldr.Build ()
let exitCode = 0
[<EntryPoint>]
let main _ =
let appRoot = Directory.GetCurrentDirectory ()
use host = WebHostBuilder() |> (Configure.webHost appRoot [| "wwwroot" |] >> Configure.buildHost)
host.Run ()
exitCode

View File

@@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:61905",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"MyPrayerJournal.Api": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Kestrel": {
"EndPoints": {
"Http": {
"Url": "http://localhost:3000"
}
}
}
}

View File

@@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View File

@@ -0,0 +1,18 @@
module.exports = {
root: true,
env: {
node: true
},
'extends': [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
}
}

23
src/MyPrayerJournal/App/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,24 @@
# my-prayer-journal-app
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

30234
src/MyPrayerJournal/App/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "my-prayer-journal",
"version": "3.0.0",
"private": true,
"description": "myPrayerJournal - Front End",
"author": "Daniel J. Summers <daniel@bitbadger.solutions>",
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"apistart": "cd ../Api && dotnet run",
"publish": "vue-cli-service build --modern && cd ../Api && dotnet publish -c Release -r linux-x64 --self-contained false",
"vue": "vue-cli-service build --modern && cd ../Api && dotnet run"
},
"dependencies": {
"core-js": "^3.6.5",
"vue": "^3.0.0",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.0-0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.18.0",
"@typescript-eslint/parser": "^4.18.0",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-typescript": "~4.5.0",
"@vue/cli-plugin-vuex": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
"eslint": "^6.7.2",
"eslint-plugin-vue": "^7.0.0",
"sass": "^1.26.5",
"sass-loader": "^8.0.2",
"typescript": "~4.1.5"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,30 @@
<template>
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</template>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
a {
font-weight: bold;
color: #2c3e50;
&.router-link-exact-active {
color: #42b983;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,65 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
@Options({
props: {
msg: String
}
})
export default class HelloWorld extends Vue {
msg!: string
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@@ -0,0 +1,6 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App).use(store).use(router).mount('#app')

View File

@@ -0,0 +1,25 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router

View File

@@ -0,0 +1,6 @@
/* eslint-disable */
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}

View File

@@ -0,0 +1,12 @@
import { createStore } from 'vuex'
export default createStore({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})

View File

@@ -0,0 +1,5 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@@ -0,0 +1,18 @@
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js + TypeScript App"/>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component';
import HelloWorld from '@/components/HelloWorld.vue'; // @ is an alias to /src
@Options({
components: {
HelloWorld,
},
})
export default class Home extends Vue {}
</script>

View File

@@ -0,0 +1,40 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"strict": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,3 @@
module.exports = {
lintOnSave: false
}