Version 2.2.2 #35

Merged
danieljsummers merged 6 commits from version-2-2-2 into main 2022-07-12 02:11:42 +00:00
8 changed files with 1317 additions and 1388 deletions
Showing only changes of commit b591bf746c - Show all commits

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ src/**/bin
src/**/obj src/**/obj
src/**/appsettings.*.json src/**/appsettings.*.json
src/.vs src/.vs
src/.idea

View File

@ -7,16 +7,12 @@ open System
open Types open Types
/// Format a GUID as a Short GUID /// Format a GUID as a Short GUID
let private toShortGuid guid = let private toShortGuid (guid : Guid) =
let convert (g : Guid) = Convert.ToBase64String(guid.ToByteArray ()).Replace('/', '_').Replace('+', '-')[0..21]
Convert.ToBase64String (g.ToByteArray ())
|> String.map (fun x -> match x with '/' -> '_' | '+' -> '-' | _ -> x)
(convert guid).Substring (0, 22)
/// Turn a Short GUID back into a GUID /// Turn a Short GUID back into a GUID
let private fromShortGuid x = let private fromShortGuid (it : string) =
let unBase64 = x |> String.map (fun x -> match x with '_' -> '/' | '-' -> '+' | _ -> x) (Convert.FromBase64String >> Guid) $"{it.Replace('_', '/').Replace('-', '+')}=="
(Convert.FromBase64String >> Guid) $"{unBase64}=="
/// Support functions for citizen IDs /// Support functions for citizen IDs
@ -24,7 +20,7 @@ module CitizenId =
/// Create a new citizen ID /// Create a new citizen ID
let create () = (Guid.NewGuid >> CitizenId) () let create () = (Guid.NewGuid >> CitizenId) ()
/// A string representation of a citizen ID /// A string representation of a citizen ID
let toString = function (CitizenId it) -> toShortGuid it let toString = function CitizenId it -> toShortGuid it
/// Parse a string into a citizen ID /// Parse a string into a citizen ID
let ofString = fromShortGuid >> CitizenId let ofString = fromShortGuid >> CitizenId
@ -43,7 +39,7 @@ module ContinentId =
/// Create a new continent ID /// Create a new continent ID
let create () = (Guid.NewGuid >> ContinentId) () let create () = (Guid.NewGuid >> ContinentId) ()
/// A string representation of a continent ID /// A string representation of a continent ID
let toString = function (ContinentId it) -> toShortGuid it let toString = function ContinentId it -> toShortGuid it
/// Parse a string into a continent ID /// Parse a string into a continent ID
let ofString = fromShortGuid >> ContinentId let ofString = fromShortGuid >> ContinentId
@ -53,7 +49,7 @@ module ListingId =
/// Create a new job listing ID /// Create a new job listing ID
let create () = (Guid.NewGuid >> ListingId) () let create () = (Guid.NewGuid >> ListingId) ()
/// A string representation of a listing ID /// A string representation of a listing ID
let toString = function (ListingId it) -> toShortGuid it let toString = function ListingId it -> toShortGuid it
/// Parse a string into a listing ID /// Parse a string into a listing ID
let ofString = fromShortGuid >> ListingId let ofString = fromShortGuid >> ListingId
@ -63,7 +59,7 @@ module MarkdownString =
/// The Markdown conversion pipeline (enables all advanced features) /// The Markdown conversion pipeline (enables all advanced features)
let private pipeline = MarkdownPipelineBuilder().UseAdvancedExtensions().Build () let private pipeline = MarkdownPipelineBuilder().UseAdvancedExtensions().Build ()
/// Convert this Markdown string to HTML /// Convert this Markdown string to HTML
let toHtml = function (Text text) -> Markdown.ToHtml (text, pipeline) let toHtml = function Text text -> Markdown.ToHtml (text, pipeline)
/// Support functions for Profiles /// Support functions for Profiles
@ -88,7 +84,7 @@ module SkillId =
/// Create a new skill ID /// Create a new skill ID
let create () = (Guid.NewGuid >> SkillId) () let create () = (Guid.NewGuid >> SkillId) ()
/// A string representation of a skill ID /// A string representation of a skill ID
let toString = function (SkillId it) -> toShortGuid it let toString = function SkillId it -> toShortGuid it
/// Parse a string into a skill ID /// Parse a string into a skill ID
let ofString = fromShortGuid >> SkillId let ofString = fromShortGuid >> SkillId
@ -98,6 +94,6 @@ module SuccessId =
/// Create a new success report ID /// Create a new success report ID
let create () = (Guid.NewGuid >> SuccessId) () let create () = (Guid.NewGuid >> SuccessId) ()
/// A string representation of a success report ID /// A string representation of a success report ID
let toString = function (SuccessId it) -> toShortGuid it let toString = function SuccessId it -> toShortGuid it
/// Parse a string into a success report ID /// Parse a string into a success report ID
let ofString = fromShortGuid >> SuccessId let ofString = fromShortGuid >> SuccessId

View File

@ -8,8 +8,8 @@ open NodaTime
// fsharplint:disable FieldNames // fsharplint:disable FieldNames
/// The data required to add or edit a job listing /// The data required to add or edit a job listing
type ListingForm = { type ListingForm =
/// The ID of the listing { /// The ID of the listing
id : string id : string
/// The listing title /// The listing title
title : string title : string
@ -27,8 +27,8 @@ type ListingForm = {
/// The data needed to display a listing /// The data needed to display a listing
type ListingForView = { type ListingForView =
/// The listing itself { /// The listing itself
listing : Listing listing : Listing
/// The continent for that listing /// The continent for that listing
continent : Continent continent : Continent
@ -36,8 +36,8 @@ type ListingForView = {
/// The form submitted to expire a listing /// The form submitted to expire a listing
type ListingExpireForm = { type ListingExpireForm =
/// Whether the job was filled from here { /// Whether the job was filled from here
fromHere : bool fromHere : bool
/// The success story written by the user /// The success story written by the user
successStory : string option successStory : string option
@ -46,8 +46,8 @@ type ListingExpireForm = {
/// The various ways job listings can be searched /// The various ways job listings can be searched
[<CLIMutable>] [<CLIMutable>]
type ListingSearch = { type ListingSearch =
/// Retrieve job listings for this continent { /// Retrieve job listings for this continent
continentId : string option continentId : string option
/// Text for a search within a region /// Text for a search within a region
region : string option region : string option
@ -59,8 +59,8 @@ type ListingSearch = {
/// A successful logon /// A successful logon
type LogOnSuccess = { type LogOnSuccess =
/// The JSON Web Token (JWT) to use for API access { /// The JSON Web Token (JWT) to use for API access
jwt : string jwt : string
/// The ID of the logged-in citizen (as a string) /// The ID of the logged-in citizen (as a string)
citizenId : string citizenId : string
@ -70,8 +70,8 @@ type LogOnSuccess = {
/// A count /// A count
type Count = { type Count =
// The count being returned { // The count being returned
count : int64 count : int64
} }
@ -92,7 +92,7 @@ type MastodonInstance () =
/// The authorization options for Jobs, Jobs, Jobs /// The authorization options for Jobs, Jobs, Jobs
type AuthOptions () = type AuthOptions () =
/// The host for the return URL for Mastodoon verification /// The host for the return URL for Mastodon verification
member val ReturnHost = "" with get, set member val ReturnHost = "" with get, set
/// The secret with which the server signs the JWTs for auth once we've verified with Mastodon /// The secret with which the server signs the JWTs for auth once we've verified with Mastodon
member val ServerSecret = "" with get, set member val ServerSecret = "" with get, set
@ -103,8 +103,8 @@ type AuthOptions () =
/// The Mastodon instance data provided via the Jobs, Jobs, Jobs API /// The Mastodon instance data provided via the Jobs, Jobs, Jobs API
type Instance = { type Instance =
/// The name of the instance { /// The name of the instance
name : string name : string
/// The URL for this instance /// The URL for this instance
url : string url : string
@ -116,8 +116,8 @@ type Instance = {
/// The fields required for a skill /// The fields required for a skill
type SkillForm = { type SkillForm =
/// The ID of this skill { /// The ID of this skill
id : string id : string
/// The description of the skill /// The description of the skill
description : string description : string
@ -127,8 +127,8 @@ type SkillForm = {
/// The data required to update a profile /// The data required to update a profile
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type ProfileForm = { type ProfileForm =
/// Whether the citizen to whom this profile belongs is actively seeking employment { /// Whether the citizen to whom this profile belongs is actively seeking employment
isSeekingEmployment : bool isSeekingEmployment : bool
/// Whether this profile should appear in the public search /// Whether this profile should appear in the public search
isPublic : bool isPublic : bool
@ -174,8 +174,8 @@ module ProfileForm =
/// The various ways profiles can be searched /// The various ways profiles can be searched
[<CLIMutable>] [<CLIMutable>]
type ProfileSearch = { type ProfileSearch =
/// Retrieve citizens from this continent { /// Retrieve citizens from this continent
continentId : string option continentId : string option
/// Text for a search within a citizen's skills /// Text for a search within a citizen's skills
skill : string option skill : string option
@ -187,8 +187,8 @@ type ProfileSearch = {
/// A user matching the profile search /// A user matching the profile search
type ProfileSearchResult = { type ProfileSearchResult =
/// The ID of the citizen { /// The ID of the citizen
citizenId : CitizenId citizenId : CitizenId
/// The citizen's display name /// The citizen's display name
displayName : string displayName : string
@ -204,8 +204,8 @@ type ProfileSearchResult = {
/// The data required to show a viewable profile /// The data required to show a viewable profile
type ProfileForView = { type ProfileForView =
/// The profile itself { /// The profile itself
profile : Profile profile : Profile
/// The citizen to whom the profile belongs /// The citizen to whom the profile belongs
citizen : Citizen citizen : Citizen
@ -216,8 +216,8 @@ type ProfileForView = {
/// The parameters for a public job search /// The parameters for a public job search
[<CLIMutable>] [<CLIMutable>]
type PublicSearch = { type PublicSearch =
/// Retrieve citizens from this continent { /// Retrieve citizens from this continent
continentId : string option continentId : string option
/// Retrieve citizens from this region /// Retrieve citizens from this region
region : string option region : string option
@ -227,20 +227,20 @@ type PublicSearch = {
remoteWork : string remoteWork : string
} }
/// Support functions for pblic searches /// Support functions for public searches
module PublicSearch = module PublicSearch =
/// Is the search empty? /// Is the search empty?
let isEmptySearch (srch : PublicSearch) = let isEmptySearch (search : PublicSearch) =
[ srch.continentId [ search.continentId
srch.skill search.skill
match srch.remoteWork with "" -> Some srch.remoteWork | _ -> None match search.remoteWork with "" -> Some search.remoteWork | _ -> None
] ]
|> List.exists Option.isSome |> List.exists Option.isSome
/// A public profile search result /// A public profile search result
type PublicSearchResult = { type PublicSearchResult =
/// The name of the continent on which the citizen resides { /// The name of the continent on which the citizen resides
continent : string continent : string
/// The region in which the citizen resides /// The region in which the citizen resides
region : string region : string
@ -252,8 +252,8 @@ type PublicSearchResult = {
/// The data required to provide a success story /// The data required to provide a success story
type StoryForm = { type StoryForm =
/// The ID of this story { /// The ID of this story
id : string id : string
/// Whether the employment was obtained from Jobs, Jobs, Jobs /// Whether the employment was obtained from Jobs, Jobs, Jobs
fromHere : bool fromHere : bool
@ -263,8 +263,8 @@ type StoryForm = {
/// An entry in the list of success stories /// An entry in the list of success stories
type StoryEntry = { type StoryEntry =
/// The ID of this success story { /// The ID of this success story
id : SuccessId id : SuccessId
/// The ID of the citizen who recorded this story /// The ID of the citizen who recorded this story
citizenId : CitizenId citizenId : CitizenId

View File

@ -11,8 +11,8 @@ type CitizenId = CitizenId of Guid
/// A user of Jobs, Jobs, Jobs /// A user of Jobs, Jobs, Jobs
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type Citizen = { type Citizen =
/// The ID of the user { /// The ID of the user
id : CitizenId id : CitizenId
/// The Mastodon instance abbreviation from which this citizen is authorized /// The Mastodon instance abbreviation from which this citizen is authorized
instance : string instance : string
@ -36,8 +36,8 @@ type ContinentId = ContinentId of Guid
/// A continent /// A continent
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type Continent = { type Continent =
/// The ID of the continent { /// The ID of the continent
id : ContinentId id : ContinentId
/// The name of the continent /// The name of the continent
name : string name : string
@ -53,8 +53,8 @@ type ListingId = ListingId of Guid
/// A job listing /// A job listing
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type Listing = { type Listing =
/// The ID of the job listing { /// The ID of the job listing
id : ListingId id : ListingId
/// The ID of the citizen who posted the job listing /// The ID of the citizen who posted the job listing
citizenId : CitizenId citizenId : CitizenId
@ -85,8 +85,8 @@ type Listing = {
type SkillId = SkillId of Guid type SkillId = SkillId of Guid
/// A skill the job seeker possesses /// A skill the job seeker possesses
type Skill = { type Skill =
/// The ID of the skill { /// The ID of the skill
id : SkillId id : SkillId
/// A description of the skill /// A description of the skill
description : string description : string
@ -97,8 +97,8 @@ type Skill = {
/// A job seeker profile /// A job seeker profile
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type Profile = { type Profile =
/// The ID of the citizen to whom this profile belongs { /// The ID of the citizen to whom this profile belongs
id : CitizenId id : CitizenId
/// Whether this citizen is actively seeking employment /// Whether this citizen is actively seeking employment
seekingEmployment : bool seekingEmployment : bool
@ -127,8 +127,8 @@ type SuccessId = SuccessId of Guid
/// A record of success finding employment /// A record of success finding employment
[<CLIMutable; NoComparison; NoEquality>] [<CLIMutable; NoComparison; NoEquality>]
type Success = { type Success =
/// The ID of the success report { /// The ID of the success report
id : SuccessId id : SuccessId
/// The ID of the citizen who wrote this success report /// The ID of the citizen who wrote this success report
citizenId : CitizenId citizenId : CitizenId

View File

@ -11,8 +11,7 @@ open Giraffe.EndpointRouting
/// Configure the ASP.NET Core pipeline to use Giraffe /// Configure the ASP.NET Core pipeline to use Giraffe
let configureApp (app : IApplicationBuilder) = let configureApp (app : IApplicationBuilder) =
app app.UseCors(fun p -> p.AllowAnyOrigin().AllowAnyHeader() |> ignore)
.UseCors(fun p -> p.AllowAnyOrigin().AllowAnyHeader() |> ignore)
.UseStaticFiles() .UseStaticFiles()
.UseRouting() .UseRouting()
.UseAuthentication() .UseAuthentication()
@ -72,8 +71,7 @@ let configureServices (svc : IServiceCollection) =
[<EntryPoint>] [<EntryPoint>]
let main _ = let main _ =
Host.CreateDefaultBuilder() Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults( .ConfigureWebHostDefaults(fun webHostBuilder ->
fun webHostBuilder ->
webHostBuilder webHostBuilder
.Configure(configureApp) .Configure(configureApp)
.ConfigureServices(configureServices) .ConfigureServices(configureServices)

View File

@ -99,7 +99,8 @@ let createJwt (citizen : Citizen) (cfg : AuthOptions) =
Issuer = "https://noagendacareers.com", Issuer = "https://noagendacareers.com",
Audience = "https://noagendacareers.com", Audience = "https://noagendacareers.com",
SigningCredentials = SigningCredentials ( SigningCredentials = SigningCredentials (
SymmetricSecurityKey (Encoding.UTF8.GetBytes cfg.ServerSecret), SecurityAlgorithms.HmacSha256Signature) SymmetricSecurityKey (
Encoding.UTF8.GetBytes cfg.ServerSecret), SecurityAlgorithms.HmacSha256Signature)
) )
) )
tokenHandler.WriteToken token tokenHandler.WriteToken token

View File

@ -25,61 +25,61 @@ module Converters =
/// JSON converter for citizen IDs /// JSON converter for citizen IDs
type CitizenIdJsonConverter() = type CitizenIdJsonConverter() =
inherit JsonConverter<CitizenId>() inherit JsonConverter<CitizenId>()
override __.WriteJson(writer : JsonWriter, value : CitizenId, _ : JsonSerializer) = override _.WriteJson(writer : JsonWriter, value : CitizenId, _ : JsonSerializer) =
writer.WriteValue (CitizenId.toString value) writer.WriteValue (CitizenId.toString value)
override __.ReadJson(reader: JsonReader, _ : Type, _ : CitizenId, _ : bool, _ : JsonSerializer) = override _.ReadJson(reader: JsonReader, _ : Type, _ : CitizenId, _ : bool, _ : JsonSerializer) =
(string >> CitizenId.ofString) reader.Value (string >> CitizenId.ofString) reader.Value
/// JSON converter for continent IDs /// JSON converter for continent IDs
type ContinentIdJsonConverter() = type ContinentIdJsonConverter() =
inherit JsonConverter<ContinentId>() inherit JsonConverter<ContinentId>()
override __.WriteJson(writer : JsonWriter, value : ContinentId, _ : JsonSerializer) = override _.WriteJson(writer : JsonWriter, value : ContinentId, _ : JsonSerializer) =
writer.WriteValue (ContinentId.toString value) writer.WriteValue (ContinentId.toString value)
override __.ReadJson(reader: JsonReader, _ : Type, _ : ContinentId, _ : bool, _ : JsonSerializer) = override _.ReadJson(reader: JsonReader, _ : Type, _ : ContinentId, _ : bool, _ : JsonSerializer) =
(string >> ContinentId.ofString) reader.Value (string >> ContinentId.ofString) reader.Value
/// JSON converter for Markdown strings /// JSON converter for Markdown strings
type MarkdownStringJsonConverter() = type MarkdownStringJsonConverter() =
inherit JsonConverter<MarkdownString>() inherit JsonConverter<MarkdownString>()
override __.WriteJson(writer : JsonWriter, value : MarkdownString, _ : JsonSerializer) = override _.WriteJson(writer : JsonWriter, value : MarkdownString, _ : JsonSerializer) =
let (Text text) = value let (Text text) = value
writer.WriteValue text writer.WriteValue text
override __.ReadJson(reader: JsonReader, _ : Type, _ : MarkdownString, _ : bool, _ : JsonSerializer) = override _.ReadJson(reader: JsonReader, _ : Type, _ : MarkdownString, _ : bool, _ : JsonSerializer) =
(string >> Text) reader.Value (string >> Text) reader.Value
/// JSON converter for listing IDs /// JSON converter for listing IDs
type ListingIdJsonConverter() = type ListingIdJsonConverter() =
inherit JsonConverter<ListingId>() inherit JsonConverter<ListingId>()
override __.WriteJson(writer : JsonWriter, value : ListingId, _ : JsonSerializer) = override _.WriteJson(writer : JsonWriter, value : ListingId, _ : JsonSerializer) =
writer.WriteValue (ListingId.toString value) writer.WriteValue (ListingId.toString value)
override __.ReadJson(reader: JsonReader, _ : Type, _ : ListingId, _ : bool, _ : JsonSerializer) = override _.ReadJson(reader: JsonReader, _ : Type, _ : ListingId, _ : bool, _ : JsonSerializer) =
(string >> ListingId.ofString) reader.Value (string >> ListingId.ofString) reader.Value
/// JSON converter for skill IDs /// JSON converter for skill IDs
type SkillIdJsonConverter() = type SkillIdJsonConverter() =
inherit JsonConverter<SkillId>() inherit JsonConverter<SkillId>()
override __.WriteJson(writer : JsonWriter, value : SkillId, _ : JsonSerializer) = override _.WriteJson(writer : JsonWriter, value : SkillId, _ : JsonSerializer) =
writer.WriteValue (SkillId.toString value) writer.WriteValue (SkillId.toString value)
override __.ReadJson(reader: JsonReader, _ : Type, _ : SkillId, _ : bool, _ : JsonSerializer) = override _.ReadJson(reader: JsonReader, _ : Type, _ : SkillId, _ : bool, _ : JsonSerializer) =
(string >> SkillId.ofString) reader.Value (string >> SkillId.ofString) reader.Value
/// JSON converter for success report IDs /// JSON converter for success report IDs
type SuccessIdJsonConverter() = type SuccessIdJsonConverter() =
inherit JsonConverter<SuccessId>() inherit JsonConverter<SuccessId>()
override __.WriteJson(writer : JsonWriter, value : SuccessId, _ : JsonSerializer) = override _.WriteJson(writer : JsonWriter, value : SuccessId, _ : JsonSerializer) =
writer.WriteValue (SuccessId.toString value) writer.WriteValue (SuccessId.toString value)
override __.ReadJson(reader: JsonReader, _ : Type, _ : SuccessId, _ : bool, _ : JsonSerializer) = override _.ReadJson(reader: JsonReader, _ : Type, _ : SuccessId, _ : bool, _ : JsonSerializer) =
(string >> SuccessId.ofString) reader.Value (string >> SuccessId.ofString) reader.Value
/// All JSON converters needed for the application /// All JSON converters needed for the application
let all () = [ let all () : JsonConverter list =
CitizenIdJsonConverter () :> JsonConverter [ CitizenIdJsonConverter ()
upcast ContinentIdJsonConverter () ContinentIdJsonConverter ()
upcast MarkdownStringJsonConverter () MarkdownStringJsonConverter ()
upcast ListingIdJsonConverter () ListingIdJsonConverter ()
upcast SkillIdJsonConverter () SkillIdJsonConverter ()
upcast SuccessIdJsonConverter () SuccessIdJsonConverter ()
upcast CompactUnionJsonConverter () CompactUnionJsonConverter ()
] ]
@ -119,11 +119,11 @@ module Startup =
// Read the configuration and create a connection // Read the configuration and create a connection
let bldr = let bldr =
seq<Connection.Builder -> Connection.Builder> { seq<Connection.Builder -> Connection.Builder> {
yield fun b -> match cfg.["Hostname"] with null -> b | host -> b.Hostname host yield fun b -> match cfg["Hostname"] with null -> b | host -> b.Hostname host
yield fun b -> match cfg.["Port"] with null -> b | port -> (int >> b.Port) port yield fun b -> match cfg["Port"] with null -> b | port -> (int >> b.Port) port
yield fun b -> match cfg.["AuthKey"] with null -> b | key -> b.AuthKey key yield fun b -> match cfg["AuthKey"] with null -> b | key -> b.AuthKey key
yield fun b -> match cfg.["Db"] with null -> b | db -> b.Db db yield fun b -> match cfg["Db"] with null -> b | db -> b.Db db
yield fun b -> match cfg.["Timeout"] with null -> b | time -> (int >> b.Timeout) time yield fun b -> match cfg["Timeout"] with null -> b | time -> (int >> b.Timeout) time
} }
|> Seq.fold (fun b step -> step b) (r.Connection ()) |> Seq.fold (fun b step -> step b) (r.Connection ())
match log.IsEnabled LogLevel.Debug with match log.IsEnabled LogLevel.Debug with
@ -134,7 +134,7 @@ module Startup =
/// Ensure the data, tables, and indexes that are required exist /// Ensure the data, tables, and indexes that are required exist
let establishEnvironment (cfg : IConfigurationSection) (log : ILogger) conn = task { let establishEnvironment (cfg : IConfigurationSection) (log : ILogger) conn = task {
// Ensure the database exists // Ensure the database exists
match cfg.["Db"] |> Option.ofObj with match cfg["Db"] |> Option.ofObj with
| Some database -> | Some database ->
let! dbs = r.DbList().RunResultAsync<string list> conn let! dbs = r.DbList().RunResultAsync<string list> conn
match dbs |> List.contains database with match dbs |> List.contains database with
@ -260,28 +260,24 @@ module Profile =
|> withReconnIgnore conn |> withReconnIgnore conn
/// Search profiles (logged-on users) /// Search profiles (logged-on users)
let search (srch : ProfileSearch) conn = let search (search : ProfileSearch) conn =
fun c -> (seq<ReqlExpr -> ReqlExpr> {
(seq { match search.continentId with
match srch.continentId with | Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId)))
| Some conId ->
yield (fun (q : ReqlExpr) ->
q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
| None -> () | None -> ()
match srch.remoteWork with match search.remoteWork with
| "" -> () | "" -> ()
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr) | _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes")))
match srch.skill with match search.skill with
| Some skl -> | Some skl ->
yield (fun q -> q.Filter (ReqlFunction1(fun it -> yield (fun q -> q.Filter (ReqlFunction1(fun it ->
upcast it.G("skills").Contains (ReqlFunction1(fun s -> it.G("skills").Contains (ReqlFunction1(fun s -> s.G("description").Match (regexContains skl))))))
upcast s.G("description").Match (regexContains skl))))) :> ReqlExpr)
| None -> () | None -> ()
match srch.bioExperience with match search.bioExperience with
| Some text -> | Some text ->
let txt = regexContains text let txt = regexContains text
yield (fun q -> q.Filter (ReqlFunction1(fun it -> yield (fun q -> q.Filter (ReqlFunction1(fun it ->
upcast it.G("biography").Match(txt).Or (it.G("experience").Match txt))) :> ReqlExpr) it.G("biography").Match(txt).Or (it.G("experience").Match txt))))
| None -> () | None -> ()
} }
|> Seq.toList |> Seq.toList
@ -300,31 +296,26 @@ module Profile =
.With ("citizenId", it.G "id"))) .With ("citizenId", it.G "id")))
.Pluck("citizenId", "displayName", "seekingEmployment", "remoteWork", "fullTime", "lastUpdatedOn") .Pluck("citizenId", "displayName", "seekingEmployment", "remoteWork", "fullTime", "lastUpdatedOn")
.OrderBy(ReqlFunction1 (fun it -> upcast it.G("displayName").Downcase ())) .OrderBy(ReqlFunction1 (fun it -> upcast it.G("displayName").Downcase ()))
.RunResultAsync<ProfileSearchResult list> c .RunResultAsync<ProfileSearchResult list>
|> withReconn conn |> withReconn conn
// Search profiles (public) // Search profiles (public)
let publicSearch (srch : PublicSearch) conn = let publicSearch (srch : PublicSearch) conn =
fun c -> (seq<ReqlExpr -> ReqlExpr> {
(seq {
match srch.continentId with match srch.continentId with
| Some conId -> | Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString cId)))
yield (fun (q : ReqlExpr) ->
q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
| None -> () | None -> ()
match srch.region with match srch.region with
| Some reg -> | Some reg ->
yield (fun q -> yield (fun q -> q.Filter (ReqlFunction1 (fun it -> upcast it.G("region").Match (regexContains reg))))
q.Filter (ReqlFunction1 (fun it -> upcast it.G("region").Match (regexContains reg))) :> ReqlExpr)
| None -> () | None -> ()
match srch.remoteWork with match srch.remoteWork with
| "" -> () | "" -> ()
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr) | _ -> yield (fun q -> q.Filter (r.HashMap (nameof srch.remoteWork, srch.remoteWork = "yes")))
match srch.skill with match srch.skill with
| Some skl -> | Some skl ->
yield (fun q -> q.Filter (ReqlFunction1 (fun it -> yield (fun q -> q.Filter (ReqlFunction1 (fun it ->
upcast it.G("skills").Contains (ReqlFunction1(fun s -> it.G("skills").Contains (ReqlFunction1(fun s -> s.G("description").Match (regexContains skl))))))
upcast s.G("description").Match (regexContains skl))))) :> ReqlExpr)
| None -> () | None -> ()
} }
|> Seq.toList |> Seq.toList
@ -334,7 +325,7 @@ module Profile =
.EqJoin("continentId", r.Table Table.Continent) .EqJoin("continentId", r.Table Table.Continent)
.Without(r.HashMap ("right", "id")) .Without(r.HashMap ("right", "id"))
.Zip() .Zip()
.Filter(r.HashMap ("isPublic", true)) :> ReqlExpr)) .Filter(r.HashMap ("isPublic", true))))
.Merge(ReqlFunction1 (fun it -> .Merge(ReqlFunction1 (fun it ->
upcast r upcast r
.HashMap("skills", .HashMap("skills",
@ -343,7 +334,7 @@ module Profile =
skill.G("description").Add(" (").Add(skill.G("notes")).Add ")")))) skill.G("description").Add(" (").Add(skill.G("notes")).Add ")"))))
.With("continent", it.G "name"))) .With("continent", it.G "name")))
.Pluck("continent", "region", "skills", "remoteWork") .Pluck("continent", "region", "skills", "remoteWork")
.RunResultAsync<PublicSearchResult list> c .RunResultAsync<PublicSearchResult list>
|> withReconn conn |> withReconn conn
/// Citizen data access functions /// Citizen data access functions
@ -445,7 +436,7 @@ module Listing =
r.Table(Table.Listing) r.Table(Table.Listing)
.GetAll(citizenId).OptArg("index", nameof citizenId) .GetAll(citizenId).OptArg("index", nameof citizenId)
.EqJoin("continentId", r.Table Table.Continent) .EqJoin("continentId", r.Table Table.Continent)
.Map(ReqlFunction1 (fun it -> upcast r.HashMap("listing", it.G "left").With ("continent", it.G "right"))) .Map(ReqlFunction1 (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right")))
.RunResultAsync<ListingForView list> .RunResultAsync<ListingForView list>
|> withReconn conn |> withReconn conn
@ -463,7 +454,7 @@ module Listing =
r.Table(Table.Listing) r.Table(Table.Listing)
.Filter(r.HashMap ("id", listingId)) .Filter(r.HashMap ("id", listingId))
.EqJoin("continentId", r.Table Table.Continent) .EqJoin("continentId", r.Table Table.Continent)
.Map(ReqlFunction1 (fun it -> upcast r.HashMap("listing", it.G "left").With ("continent", it.G "right"))) .Map(ReqlFunction1 (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right")))
.RunResultAsync<ListingForView list> c .RunResultAsync<ListingForView list> c
return List.tryHead listing return List.tryHead listing
} }
@ -493,39 +484,33 @@ module Listing =
|> withReconnIgnore conn |> withReconnIgnore conn
/// Search job listings /// Search job listings
let search (srch : ListingSearch) conn = let search (search : ListingSearch) conn =
fun c -> (seq<ReqlExpr -> ReqlExpr> {
(seq { match search.continentId with
match srch.continentId with | Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId)))
| Some conId ->
yield (fun (q : ReqlExpr) ->
q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
| None -> () | None -> ()
match srch.region with match search.region with
| Some rgn -> | Some rgn ->
yield (fun q -> yield (fun q ->
q.Filter (ReqlFunction1 (fun it -> q.Filter (ReqlFunction1 (fun it -> it.G(nameof search.region).Match (regexContains rgn))))
upcast it.G(nameof srch.region).Match (regexContains rgn))) :> ReqlExpr)
| None -> () | None -> ()
match srch.remoteWork with match search.remoteWork with
| "" -> () | "" -> ()
| _ -> | _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes")))
yield (fun q -> q.Filter (r.HashMap (nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr) match search.text with
match srch.text with
| Some text -> | Some text ->
yield (fun q -> yield (fun q ->
q.Filter (ReqlFunction1 (fun it -> q.Filter (ReqlFunction1 (fun it -> it.G(nameof search.text).Match (regexContains text))))
upcast it.G(nameof srch.text).Match (regexContains text))) :> ReqlExpr)
| None -> () | None -> ()
} }
|> Seq.toList |> Seq.toList
|> List.fold |> List.fold
(fun q f -> f q) (fun q f -> f q)
(r.Table(Table.Listing) (r.Table(Table.Listing)
.GetAll(false).OptArg ("index", "isExpired") :> ReqlExpr)) .GetAll(false).OptArg ("index", "isExpired")))
.EqJoin("continentId", r.Table Table.Continent) .EqJoin("continentId", r.Table Table.Continent)
.Map(ReqlFunction1 (fun it -> upcast r.HashMap("listing", it.G "left").With ("continent", it.G "right"))) .Map(ReqlFunction1 (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right")))
.RunResultAsync<ListingForView list> c .RunResultAsync<ListingForView list>
|> withReconn conn |> withReconn conn
@ -555,8 +540,7 @@ module Success =
.Without(r.HashMap ("right", "id")) .Without(r.HashMap ("right", "id"))
.Zip() .Zip()
.Merge(ReqlFunction1 (fun it -> .Merge(ReqlFunction1 (fun it ->
upcast r r.HashMap("citizenName",
.HashMap("citizenName",
r.Branch(it.G("realName" ).Default_("").Ne "", it.G "realName", r.Branch(it.G("realName" ).Default_("").Ne "", it.G "realName",
it.G("displayName").Default_("").Ne "", it.G "displayName", it.G("displayName").Default_("").Ne "", it.G "displayName",
it.G "mastodonUser")) it.G "mastodonUser"))

View File

@ -21,14 +21,13 @@ module Error =
open System.Threading.Tasks open System.Threading.Tasks
/// URL prefixes for the Vue app /// URL prefixes for the Vue app
let vueUrls = [ let vueUrls =
"/how-it-works"; "/privacy-policy"; "/terms-of-service"; "/citizen"; "/help-wanted"; "/listing"; "/profile" [ "/how-it-works"; "/privacy-policy"; "/terms-of-service"; "/citizen"; "/help-wanted"; "/listing"; "/profile"
"/so-long"; "/success-story" "/so-long"; "/success-story"
] ]
/// Handler that will return a status code 404 and the text "Not Found" /// Handler that will return a status code 404 and the text "Not Found"
let notFound : HttpHandler = let notFound : HttpHandler = fun next ctx -> task {
fun next ctx -> task {
let fac = ctx.GetService<ILoggerFactory> () let fac = ctx.GetService<ILoggerFactory> ()
let log = fac.CreateLogger "Handler" let log = fac.CreateLogger "Handler"
let path = string ctx.Request.Path let path = string ctx.Request.Path
@ -107,8 +106,7 @@ module Helpers =
module Citizen = module Citizen =
// GET: /api/citizen/log-on/[code] // GET: /api/citizen/log-on/[code]
let logOn (abbr, authCode) : HttpHandler = let logOn (abbr, authCode) : HttpHandler = fun next ctx -> task {
fun next ctx -> task {
// Step 1 - Verify with Mastodon // Step 1 - Verify with Mastodon
let cfg = authConfig ctx let cfg = authConfig ctx
@ -154,18 +152,14 @@ module Citizen =
} }
// GET: /api/citizen/[id] // GET: /api/citizen/[id]
let get citizenId : HttpHandler = let get citizenId : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
match! Data.Citizen.findById (CitizenId citizenId) (conn ctx) with match! Data.Citizen.findById (CitizenId citizenId) (conn ctx) with
| Some citizen -> return! json citizen next ctx | Some citizen -> return! json citizen next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// DELETE: /api/citizen // DELETE: /api/citizen
let delete : HttpHandler = let delete : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
do! Data.Citizen.delete (currentCitizenId ctx) (conn ctx) do! Data.Citizen.delete (currentCitizenId ctx) (conn ctx)
return! ok next ctx return! ok next ctx
} }
@ -176,8 +170,7 @@ module Citizen =
module Continent = module Continent =
// GET: /api/continent/all // GET: /api/continent/all
let all : HttpHandler = let all : HttpHandler = fun next ctx -> task {
fun next ctx -> task {
let! continents = Data.Continent.all (conn ctx) let! continents = Data.Continent.all (conn ctx)
return! json continents next ctx return! json continents next ctx
} }
@ -187,7 +180,7 @@ module Continent =
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module Instances = module Instances =
/// Convert a Masotodon instance to the one we use in the API /// Convert a Mastodon instance to the one we use in the API
let private toInstance (inst : MastodonInstance) = let private toInstance (inst : MastodonInstance) =
{ name = inst.Name { name = inst.Name
url = inst.Url url = inst.Url
@ -196,8 +189,7 @@ module Instances =
} }
// GET: /api/instances // GET: /api/instances
let all : HttpHandler = let all : HttpHandler = fun next ctx -> task {
fun next ctx -> task {
return! json ((authConfig ctx).Instances |> Array.map toInstance) next ctx return! json ((authConfig ctx).Instances |> Array.map toInstance) next ctx
} }
@ -213,35 +205,27 @@ module Listing =
let private parseDate = DateTime.Parse >> LocalDate.FromDateTime let private parseDate = DateTime.Parse >> LocalDate.FromDateTime
// GET: /api/listings/mine // GET: /api/listings/mine
let mine : HttpHandler = let mine : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let! listings = Data.Listing.findByCitizen (currentCitizenId ctx) (conn ctx) let! listings = Data.Listing.findByCitizen (currentCitizenId ctx) (conn ctx)
return! json listings next ctx return! json listings next ctx
} }
// GET: /api/listing/[id] // GET: /api/listing/[id]
let get listingId : HttpHandler = let get listingId : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
match! Data.Listing.findById (ListingId listingId) (conn ctx) with match! Data.Listing.findById (ListingId listingId) (conn ctx) with
| Some listing -> return! json listing next ctx | Some listing -> return! json listing next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// GET: /api/listing/view/[id] // GET: /api/listing/view/[id]
let view listingId : HttpHandler = let view listingId : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
match! Data.Listing.findByIdForView (ListingId listingId) (conn ctx) with match! Data.Listing.findByIdForView (ListingId listingId) (conn ctx) with
| Some listing -> return! json listing next ctx | Some listing -> return! json listing next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// POST: /listings // POST: /listings
let add : HttpHandler = let add : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let! form = ctx.BindJsonAsync<ListingForm> () let! form = ctx.BindJsonAsync<ListingForm> ()
let now = (clock ctx).GetCurrentInstant () let now = (clock ctx).GetCurrentInstant ()
do! Data.Listing.add do! Data.Listing.add
@ -262,9 +246,7 @@ module Listing =
} }
// PUT: /api/listing/[id] // PUT: /api/listing/[id]
let update listingId : HttpHandler = let update listingId : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let dbConn = conn ctx let dbConn = conn ctx
match! Data.Listing.findById (ListingId listingId) dbConn with match! Data.Listing.findById (ListingId listingId) dbConn with
| Some listing when listing.citizenId <> (currentCitizenId ctx) -> return! Error.notAuthorized next ctx | Some listing when listing.citizenId <> (currentCitizenId ctx) -> return! Error.notAuthorized next ctx
@ -285,9 +267,7 @@ module Listing =
} }
// PATCH: /api/listing/[id] // PATCH: /api/listing/[id]
let expire listingId : HttpHandler = let expire listingId : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> FSharp.Control.Tasks.Affine.task {
let dbConn = conn ctx let dbConn = conn ctx
let now = clock(ctx).GetCurrentInstant () let now = clock(ctx).GetCurrentInstant ()
match! Data.Listing.findById (ListingId listingId) dbConn with match! Data.Listing.findById (ListingId listingId) dbConn with
@ -311,9 +291,7 @@ module Listing =
} }
// GET: /api/listing/search // GET: /api/listing/search
let search : HttpHandler = let search : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let search = ctx.BindQueryString<ListingSearch> () let search = ctx.BindQueryString<ListingSearch> ()
let! results = Data.Listing.search search (conn ctx) let! results = Data.Listing.search search (conn ctx)
return! json results next ctx return! json results next ctx
@ -327,27 +305,21 @@ module Profile =
// GET: /api/profile // GET: /api/profile
// This returns the current citizen's profile, or a 204 if it is not found (a citizen not having a profile yet // This returns the current citizen's profile, or a 204 if it is not found (a citizen not having a profile yet
// is not an error). The "get" handler returns a 404 if a profile is not found. // is not an error). The "get" handler returns a 404 if a profile is not found.
let current : HttpHandler = let current : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
match! Data.Profile.findById (currentCitizenId ctx) (conn ctx) with match! Data.Profile.findById (currentCitizenId ctx) (conn ctx) with
| Some profile -> return! json profile next ctx | Some profile -> return! json profile next ctx
| None -> return! Successful.NO_CONTENT next ctx | None -> return! Successful.NO_CONTENT next ctx
} }
// GET: /api/profile/get/[id] // GET: /api/profile/get/[id]
let get citizenId : HttpHandler = let get citizenId : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
match! Data.Profile.findById (CitizenId citizenId) (conn ctx) with match! Data.Profile.findById (CitizenId citizenId) (conn ctx) with
| Some profile -> return! json profile next ctx | Some profile -> return! json profile next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// GET: /api/profile/view/[id] // GET: /api/profile/view/[id]
let view citizenId : HttpHandler = let view citizenId : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let citId = CitizenId citizenId let citId = CitizenId citizenId
let dbConn = conn ctx let dbConn = conn ctx
match! Data.Profile.findById citId dbConn with match! Data.Profile.findById citId dbConn with
@ -357,8 +329,8 @@ module Profile =
match! Data.Continent.findById profile.continentId dbConn with match! Data.Continent.findById profile.continentId dbConn with
| Some continent -> | Some continent ->
return! return!
json { json
profile = profile { profile = profile
citizen = citizen citizen = citizen
continent = continent continent = continent
} next ctx } next ctx
@ -368,17 +340,13 @@ module Profile =
} }
// GET: /api/profile/count // GET: /api/profile/count
let count : HttpHandler = let count : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let! theCount = Data.Profile.count (conn ctx) let! theCount = Data.Profile.count (conn ctx)
return! json { count = theCount } next ctx return! json { count = theCount } next ctx
} }
// POST: /api/profile/save // POST: /api/profile/save
let save : HttpHandler = let save : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let citizenId = currentCitizenId ctx let citizenId = currentCitizenId ctx
let dbConn = conn ctx let dbConn = conn ctx
let! form = ctx.BindJsonAsync<ProfileForm>() let! form = ctx.BindJsonAsync<ProfileForm>()
@ -412,9 +380,7 @@ module Profile =
} }
// PATCH: /api/profile/employment-found // PATCH: /api/profile/employment-found
let employmentFound : HttpHandler = let employmentFound : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let dbConn = conn ctx let dbConn = conn ctx
match! Data.Profile.findById (currentCitizenId ctx) dbConn with match! Data.Profile.findById (currentCitizenId ctx) dbConn with
| Some profile -> | Some profile ->
@ -424,25 +390,20 @@ module Profile =
} }
// DELETE: /api/profile // DELETE: /api/profile
let delete : HttpHandler = let delete : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
do! Data.Profile.delete (currentCitizenId ctx) (conn ctx) do! Data.Profile.delete (currentCitizenId ctx) (conn ctx)
return! ok next ctx return! ok next ctx
} }
// GET: /api/profile/search // GET: /api/profile/search
let search : HttpHandler = let search : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let search = ctx.BindQueryString<ProfileSearch> () let search = ctx.BindQueryString<ProfileSearch> ()
let! results = Data.Profile.search search (conn ctx) let! results = Data.Profile.search search (conn ctx)
return! json results next ctx return! json results next ctx
} }
// GET: /api/profile/public-search // GET: /api/profile/public-search
let publicSearch : HttpHandler = let publicSearch : HttpHandler = fun next ctx -> task {
fun next ctx -> task {
let search = ctx.BindQueryString<PublicSearch> () let search = ctx.BindQueryString<PublicSearch> ()
let! results = Data.Profile.publicSearch search (conn ctx) let! results = Data.Profile.publicSearch search (conn ctx)
return! json results next ctx return! json results next ctx
@ -456,26 +417,20 @@ module Success =
open System open System
// GET: /api/success/[id] // GET: /api/success/[id]
let get successId : HttpHandler = let get successId : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
match! Data.Success.findById (SuccessId successId) (conn ctx) with match! Data.Success.findById (SuccessId successId) (conn ctx) with
| Some story -> return! json story next ctx | Some story -> return! json story next ctx
| None -> return! Error.notFound next ctx | None -> return! Error.notFound next ctx
} }
// GET: /api/success/list // GET: /api/success/list
let all : HttpHandler = let all : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let! stories = Data.Success.all (conn ctx) let! stories = Data.Success.all (conn ctx)
return! json stories next ctx return! json stories next ctx
} }
// POST: /api/success/save // POST: /api/success/save
let save : HttpHandler = let save : HttpHandler = authorize >=> fun next ctx -> task {
authorize
>=> fun next ctx -> task {
let citizenId = currentCitizenId ctx let citizenId = currentCitizenId ctx
let dbConn = conn ctx let dbConn = conn ctx
let now = (clock ctx).GetCurrentInstant () let now = (clock ctx).GetCurrentInstant ()
@ -528,15 +483,9 @@ let allEndpoints = [
routef "/%O/view" Listing.view routef "/%O/view" Listing.view
route "s/mine" Listing.mine route "s/mine" Listing.mine
] ]
PATCH [ PATCH [ routef "/%O" Listing.expire ]
routef "/%O" Listing.expire POST [ route "s" Listing.add ]
] PUT [ routef "/%O" Listing.update ]
POST [
route "s" Listing.add
]
PUT [
routef "/%O" Listing.update
]
] ]
subRoute "/profile" [ subRoute "/profile" [
GET_HEAD [ GET_HEAD [