Version 2.2.2 #35
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -4,3 +4,4 @@ src/**/bin
|
|||
src/**/obj
|
||||
src/**/appsettings.*.json
|
||||
src/.vs
|
||||
src/.idea
|
||||
|
|
|
@ -7,16 +7,12 @@ open System
|
|||
open Types
|
||||
|
||||
/// Format a GUID as a Short GUID
|
||||
let private toShortGuid guid =
|
||||
let convert (g : Guid) =
|
||||
Convert.ToBase64String (g.ToByteArray ())
|
||||
|> String.map (fun x -> match x with '/' -> '_' | '+' -> '-' | _ -> x)
|
||||
(convert guid).Substring (0, 22)
|
||||
let private toShortGuid (guid : Guid) =
|
||||
Convert.ToBase64String(guid.ToByteArray ()).Replace('/', '_').Replace('+', '-')[0..21]
|
||||
|
||||
/// Turn a Short GUID back into a GUID
|
||||
let private fromShortGuid x =
|
||||
let unBase64 = x |> String.map (fun x -> match x with '_' -> '/' | '-' -> '+' | _ -> x)
|
||||
(Convert.FromBase64String >> Guid) $"{unBase64}=="
|
||||
let private fromShortGuid (it : string) =
|
||||
(Convert.FromBase64String >> Guid) $"{it.Replace('_', '/').Replace('-', '+')}=="
|
||||
|
||||
|
||||
/// Support functions for citizen IDs
|
||||
|
@ -24,7 +20,7 @@ module CitizenId =
|
|||
/// Create a new citizen ID
|
||||
let create () = (Guid.NewGuid >> CitizenId) ()
|
||||
/// 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
|
||||
let ofString = fromShortGuid >> CitizenId
|
||||
|
||||
|
@ -43,7 +39,7 @@ module ContinentId =
|
|||
/// Create a new continent ID
|
||||
let create () = (Guid.NewGuid >> ContinentId) ()
|
||||
/// 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
|
||||
let ofString = fromShortGuid >> ContinentId
|
||||
|
||||
|
@ -53,7 +49,7 @@ module ListingId =
|
|||
/// Create a new job listing ID
|
||||
let create () = (Guid.NewGuid >> ListingId) ()
|
||||
/// 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
|
||||
let ofString = fromShortGuid >> ListingId
|
||||
|
||||
|
@ -63,7 +59,7 @@ module MarkdownString =
|
|||
/// The Markdown conversion pipeline (enables all advanced features)
|
||||
let private pipeline = MarkdownPipelineBuilder().UseAdvancedExtensions().Build ()
|
||||
/// 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
|
||||
|
@ -88,7 +84,7 @@ module SkillId =
|
|||
/// Create a new skill ID
|
||||
let create () = (Guid.NewGuid >> SkillId) ()
|
||||
/// 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
|
||||
let ofString = fromShortGuid >> SkillId
|
||||
|
||||
|
@ -98,6 +94,6 @@ module SuccessId =
|
|||
/// Create a new success report ID
|
||||
let create () = (Guid.NewGuid >> SuccessId) ()
|
||||
/// 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
|
||||
let ofString = fromShortGuid >> SuccessId
|
||||
|
|
|
@ -8,8 +8,8 @@ open NodaTime
|
|||
// fsharplint:disable FieldNames
|
||||
|
||||
/// The data required to add or edit a job listing
|
||||
type ListingForm = {
|
||||
/// The ID of the listing
|
||||
type ListingForm =
|
||||
{ /// The ID of the listing
|
||||
id : string
|
||||
/// The listing title
|
||||
title : string
|
||||
|
@ -27,27 +27,27 @@ type ListingForm = {
|
|||
|
||||
|
||||
/// The data needed to display a listing
|
||||
type ListingForView = {
|
||||
/// The listing itself
|
||||
type ListingForView =
|
||||
{ /// The listing itself
|
||||
listing : Listing
|
||||
/// The continent for that listing
|
||||
continent : Continent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The form submitted to expire a listing
|
||||
type ListingExpireForm = {
|
||||
/// Whether the job was filled from here
|
||||
type ListingExpireForm =
|
||||
{ /// Whether the job was filled from here
|
||||
fromHere : bool
|
||||
/// The success story written by the user
|
||||
successStory : string option
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The various ways job listings can be searched
|
||||
[<CLIMutable>]
|
||||
type ListingSearch = {
|
||||
/// Retrieve job listings for this continent
|
||||
type ListingSearch =
|
||||
{ /// Retrieve job listings for this continent
|
||||
continentId : string option
|
||||
/// Text for a search within a region
|
||||
region : string option
|
||||
|
@ -59,8 +59,8 @@ type ListingSearch = {
|
|||
|
||||
|
||||
/// A successful logon
|
||||
type LogOnSuccess = {
|
||||
/// The JSON Web Token (JWT) to use for API access
|
||||
type LogOnSuccess =
|
||||
{ /// The JSON Web Token (JWT) to use for API access
|
||||
jwt : string
|
||||
/// The ID of the logged-in citizen (as a string)
|
||||
citizenId : string
|
||||
|
@ -70,8 +70,8 @@ type LogOnSuccess = {
|
|||
|
||||
|
||||
/// A count
|
||||
type Count = {
|
||||
// The count being returned
|
||||
type Count =
|
||||
{ // The count being returned
|
||||
count : int64
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ type MastodonInstance () =
|
|||
|
||||
/// The authorization options for Jobs, Jobs, Jobs
|
||||
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
|
||||
/// The secret with which the server signs the JWTs for auth once we've verified with Mastodon
|
||||
member val ServerSecret = "" with get, set
|
||||
|
@ -103,8 +103,8 @@ type AuthOptions () =
|
|||
|
||||
|
||||
/// The Mastodon instance data provided via the Jobs, Jobs, Jobs API
|
||||
type Instance = {
|
||||
/// The name of the instance
|
||||
type Instance =
|
||||
{ /// The name of the instance
|
||||
name : string
|
||||
/// The URL for this instance
|
||||
url : string
|
||||
|
@ -116,8 +116,8 @@ type Instance = {
|
|||
|
||||
|
||||
/// The fields required for a skill
|
||||
type SkillForm = {
|
||||
/// The ID of this skill
|
||||
type SkillForm =
|
||||
{ /// The ID of this skill
|
||||
id : string
|
||||
/// The description of the skill
|
||||
description : string
|
||||
|
@ -127,8 +127,8 @@ type SkillForm = {
|
|||
|
||||
/// The data required to update a profile
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type ProfileForm = {
|
||||
/// Whether the citizen to whom this profile belongs is actively seeking employment
|
||||
type ProfileForm =
|
||||
{ /// Whether the citizen to whom this profile belongs is actively seeking employment
|
||||
isSeekingEmployment : bool
|
||||
/// Whether this profile should appear in the public search
|
||||
isPublic : bool
|
||||
|
@ -174,8 +174,8 @@ module ProfileForm =
|
|||
|
||||
/// The various ways profiles can be searched
|
||||
[<CLIMutable>]
|
||||
type ProfileSearch = {
|
||||
/// Retrieve citizens from this continent
|
||||
type ProfileSearch =
|
||||
{ /// Retrieve citizens from this continent
|
||||
continentId : string option
|
||||
/// Text for a search within a citizen's skills
|
||||
skill : string option
|
||||
|
@ -187,8 +187,8 @@ type ProfileSearch = {
|
|||
|
||||
|
||||
/// A user matching the profile search
|
||||
type ProfileSearchResult = {
|
||||
/// The ID of the citizen
|
||||
type ProfileSearchResult =
|
||||
{ /// The ID of the citizen
|
||||
citizenId : CitizenId
|
||||
/// The citizen's display name
|
||||
displayName : string
|
||||
|
@ -204,20 +204,20 @@ type ProfileSearchResult = {
|
|||
|
||||
|
||||
/// The data required to show a viewable profile
|
||||
type ProfileForView = {
|
||||
/// The profile itself
|
||||
type ProfileForView =
|
||||
{ /// The profile itself
|
||||
profile : Profile
|
||||
/// The citizen to whom the profile belongs
|
||||
citizen : Citizen
|
||||
/// The continent for the profile
|
||||
continent : Continent
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The parameters for a public job search
|
||||
[<CLIMutable>]
|
||||
type PublicSearch = {
|
||||
/// Retrieve citizens from this continent
|
||||
type PublicSearch =
|
||||
{ /// Retrieve citizens from this continent
|
||||
continentId : string option
|
||||
/// Retrieve citizens from this region
|
||||
region : string option
|
||||
|
@ -227,20 +227,20 @@ type PublicSearch = {
|
|||
remoteWork : string
|
||||
}
|
||||
|
||||
/// Support functions for pblic searches
|
||||
/// Support functions for public searches
|
||||
module PublicSearch =
|
||||
/// Is the search empty?
|
||||
let isEmptySearch (srch : PublicSearch) =
|
||||
[ srch.continentId
|
||||
srch.skill
|
||||
match srch.remoteWork with "" -> Some srch.remoteWork | _ -> None
|
||||
let isEmptySearch (search : PublicSearch) =
|
||||
[ search.continentId
|
||||
search.skill
|
||||
match search.remoteWork with "" -> Some search.remoteWork | _ -> None
|
||||
]
|
||||
|> List.exists Option.isSome
|
||||
|
||||
|
||||
/// A public profile search result
|
||||
type PublicSearchResult = {
|
||||
/// The name of the continent on which the citizen resides
|
||||
type PublicSearchResult =
|
||||
{ /// The name of the continent on which the citizen resides
|
||||
continent : string
|
||||
/// The region in which the citizen resides
|
||||
region : string
|
||||
|
@ -252,8 +252,8 @@ type PublicSearchResult = {
|
|||
|
||||
|
||||
/// The data required to provide a success story
|
||||
type StoryForm = {
|
||||
/// The ID of this story
|
||||
type StoryForm =
|
||||
{ /// The ID of this story
|
||||
id : string
|
||||
/// Whether the employment was obtained from Jobs, Jobs, Jobs
|
||||
fromHere : bool
|
||||
|
@ -263,8 +263,8 @@ type StoryForm = {
|
|||
|
||||
|
||||
/// An entry in the list of success stories
|
||||
type StoryEntry = {
|
||||
/// The ID of this success story
|
||||
type StoryEntry =
|
||||
{ /// The ID of this success story
|
||||
id : SuccessId
|
||||
/// The ID of the citizen who recorded this story
|
||||
citizenId : CitizenId
|
||||
|
|
|
@ -11,8 +11,8 @@ type CitizenId = CitizenId of Guid
|
|||
|
||||
/// A user of Jobs, Jobs, Jobs
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Citizen = {
|
||||
/// The ID of the user
|
||||
type Citizen =
|
||||
{ /// The ID of the user
|
||||
id : CitizenId
|
||||
/// The Mastodon instance abbreviation from which this citizen is authorized
|
||||
instance : string
|
||||
|
@ -36,8 +36,8 @@ type ContinentId = ContinentId of Guid
|
|||
|
||||
/// A continent
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Continent = {
|
||||
/// The ID of the continent
|
||||
type Continent =
|
||||
{ /// The ID of the continent
|
||||
id : ContinentId
|
||||
/// The name of the continent
|
||||
name : string
|
||||
|
@ -53,8 +53,8 @@ type ListingId = ListingId of Guid
|
|||
|
||||
/// A job listing
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Listing = {
|
||||
/// The ID of the job listing
|
||||
type Listing =
|
||||
{ /// The ID of the job listing
|
||||
id : ListingId
|
||||
/// The ID of the citizen who posted the job listing
|
||||
citizenId : CitizenId
|
||||
|
@ -85,8 +85,8 @@ type Listing = {
|
|||
type SkillId = SkillId of Guid
|
||||
|
||||
/// A skill the job seeker possesses
|
||||
type Skill = {
|
||||
/// The ID of the skill
|
||||
type Skill =
|
||||
{ /// The ID of the skill
|
||||
id : SkillId
|
||||
/// A description of the skill
|
||||
description : string
|
||||
|
@ -97,8 +97,8 @@ type Skill = {
|
|||
|
||||
/// A job seeker profile
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Profile = {
|
||||
/// The ID of the citizen to whom this profile belongs
|
||||
type Profile =
|
||||
{ /// The ID of the citizen to whom this profile belongs
|
||||
id : CitizenId
|
||||
/// Whether this citizen is actively seeking employment
|
||||
seekingEmployment : bool
|
||||
|
@ -127,8 +127,8 @@ type SuccessId = SuccessId of Guid
|
|||
|
||||
/// A record of success finding employment
|
||||
[<CLIMutable; NoComparison; NoEquality>]
|
||||
type Success = {
|
||||
/// The ID of the success report
|
||||
type Success =
|
||||
{ /// The ID of the success report
|
||||
id : SuccessId
|
||||
/// The ID of the citizen who wrote this success report
|
||||
citizenId : CitizenId
|
||||
|
|
|
@ -11,8 +11,7 @@ open Giraffe.EndpointRouting
|
|||
|
||||
/// Configure the ASP.NET Core pipeline to use Giraffe
|
||||
let configureApp (app : IApplicationBuilder) =
|
||||
app
|
||||
.UseCors(fun p -> p.AllowAnyOrigin().AllowAnyHeader() |> ignore)
|
||||
app.UseCors(fun p -> p.AllowAnyOrigin().AllowAnyHeader() |> ignore)
|
||||
.UseStaticFiles()
|
||||
.UseRouting()
|
||||
.UseAuthentication()
|
||||
|
@ -72,8 +71,7 @@ let configureServices (svc : IServiceCollection) =
|
|||
[<EntryPoint>]
|
||||
let main _ =
|
||||
Host.CreateDefaultBuilder()
|
||||
.ConfigureWebHostDefaults(
|
||||
fun webHostBuilder ->
|
||||
.ConfigureWebHostDefaults(fun webHostBuilder ->
|
||||
webHostBuilder
|
||||
.Configure(configureApp)
|
||||
.ConfigureServices(configureServices)
|
||||
|
|
|
@ -42,7 +42,7 @@ let verifyWithMastodon (authCode : string) (inst : MastodonInstance) rtnHost (lo
|
|||
|
||||
// Use authorization code to get an access token from Mastodon
|
||||
use! codeResult =
|
||||
http.PostAsJsonAsync($"{inst.Url}/oauth/token",
|
||||
http.PostAsJsonAsync ($"{inst.Url}/oauth/token",
|
||||
{| client_id = inst.ClientId
|
||||
client_secret = inst.Secret
|
||||
redirect_uri = $"{rtnHost}/citizen/{inst.Abbr}/authorized"
|
||||
|
@ -99,7 +99,8 @@ let createJwt (citizen : Citizen) (cfg : AuthOptions) =
|
|||
Issuer = "https://noagendacareers.com",
|
||||
Audience = "https://noagendacareers.com",
|
||||
SigningCredentials = SigningCredentials (
|
||||
SymmetricSecurityKey (Encoding.UTF8.GetBytes cfg.ServerSecret), SecurityAlgorithms.HmacSha256Signature)
|
||||
SymmetricSecurityKey (
|
||||
Encoding.UTF8.GetBytes cfg.ServerSecret), SecurityAlgorithms.HmacSha256Signature)
|
||||
)
|
||||
)
|
||||
tokenHandler.WriteToken token
|
||||
|
|
|
@ -25,61 +25,61 @@ module Converters =
|
|||
/// JSON converter for citizen IDs
|
||||
type CitizenIdJsonConverter() =
|
||||
inherit JsonConverter<CitizenId>()
|
||||
override __.WriteJson(writer : JsonWriter, value : CitizenId, _ : JsonSerializer) =
|
||||
override _.WriteJson(writer : JsonWriter, value : CitizenId, _ : JsonSerializer) =
|
||||
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
|
||||
|
||||
/// JSON converter for continent IDs
|
||||
type ContinentIdJsonConverter() =
|
||||
inherit JsonConverter<ContinentId>()
|
||||
override __.WriteJson(writer : JsonWriter, value : ContinentId, _ : JsonSerializer) =
|
||||
override _.WriteJson(writer : JsonWriter, value : ContinentId, _ : JsonSerializer) =
|
||||
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
|
||||
|
||||
/// JSON converter for Markdown strings
|
||||
type MarkdownStringJsonConverter() =
|
||||
inherit JsonConverter<MarkdownString>()
|
||||
override __.WriteJson(writer : JsonWriter, value : MarkdownString, _ : JsonSerializer) =
|
||||
override _.WriteJson(writer : JsonWriter, value : MarkdownString, _ : JsonSerializer) =
|
||||
let (Text text) = value
|
||||
writer.WriteValue text
|
||||
override __.ReadJson(reader: JsonReader, _ : Type, _ : MarkdownString, _ : bool, _ : JsonSerializer) =
|
||||
override _.ReadJson(reader: JsonReader, _ : Type, _ : MarkdownString, _ : bool, _ : JsonSerializer) =
|
||||
(string >> Text) reader.Value
|
||||
|
||||
/// JSON converter for listing IDs
|
||||
type ListingIdJsonConverter() =
|
||||
inherit JsonConverter<ListingId>()
|
||||
override __.WriteJson(writer : JsonWriter, value : ListingId, _ : JsonSerializer) =
|
||||
override _.WriteJson(writer : JsonWriter, value : ListingId, _ : JsonSerializer) =
|
||||
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
|
||||
|
||||
/// JSON converter for skill IDs
|
||||
type SkillIdJsonConverter() =
|
||||
inherit JsonConverter<SkillId>()
|
||||
override __.WriteJson(writer : JsonWriter, value : SkillId, _ : JsonSerializer) =
|
||||
override _.WriteJson(writer : JsonWriter, value : SkillId, _ : JsonSerializer) =
|
||||
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
|
||||
|
||||
/// JSON converter for success report IDs
|
||||
type SuccessIdJsonConverter() =
|
||||
inherit JsonConverter<SuccessId>()
|
||||
override __.WriteJson(writer : JsonWriter, value : SuccessId, _ : JsonSerializer) =
|
||||
override _.WriteJson(writer : JsonWriter, value : SuccessId, _ : JsonSerializer) =
|
||||
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
|
||||
|
||||
/// All JSON converters needed for the application
|
||||
let all () = [
|
||||
CitizenIdJsonConverter () :> JsonConverter
|
||||
upcast ContinentIdJsonConverter ()
|
||||
upcast MarkdownStringJsonConverter ()
|
||||
upcast ListingIdJsonConverter ()
|
||||
upcast SkillIdJsonConverter ()
|
||||
upcast SuccessIdJsonConverter ()
|
||||
upcast CompactUnionJsonConverter ()
|
||||
let all () : JsonConverter list =
|
||||
[ CitizenIdJsonConverter ()
|
||||
ContinentIdJsonConverter ()
|
||||
MarkdownStringJsonConverter ()
|
||||
ListingIdJsonConverter ()
|
||||
SkillIdJsonConverter ()
|
||||
SuccessIdJsonConverter ()
|
||||
CompactUnionJsonConverter ()
|
||||
]
|
||||
|
||||
|
||||
|
@ -119,11 +119,11 @@ module Startup =
|
|||
// Read the configuration and create a connection
|
||||
let bldr =
|
||||
seq<Connection.Builder -> Connection.Builder> {
|
||||
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.["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.["Timeout"] with null -> b | time -> (int >> b.Timeout) time
|
||||
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["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["Timeout"] with null -> b | time -> (int >> b.Timeout) time
|
||||
}
|
||||
|> Seq.fold (fun b step -> step b) (r.Connection ())
|
||||
match log.IsEnabled LogLevel.Debug with
|
||||
|
@ -134,7 +134,7 @@ module Startup =
|
|||
/// Ensure the data, tables, and indexes that are required exist
|
||||
let establishEnvironment (cfg : IConfigurationSection) (log : ILogger) conn = task {
|
||||
// Ensure the database exists
|
||||
match cfg.["Db"] |> Option.ofObj with
|
||||
match cfg["Db"] |> Option.ofObj with
|
||||
| Some database ->
|
||||
let! dbs = r.DbList().RunResultAsync<string list> conn
|
||||
match dbs |> List.contains database with
|
||||
|
@ -260,28 +260,24 @@ module Profile =
|
|||
|> withReconnIgnore conn
|
||||
|
||||
/// Search profiles (logged-on users)
|
||||
let search (srch : ProfileSearch) conn =
|
||||
fun c ->
|
||||
(seq {
|
||||
match srch.continentId with
|
||||
| Some conId ->
|
||||
yield (fun (q : ReqlExpr) ->
|
||||
q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
|
||||
let search (search : ProfileSearch) conn =
|
||||
(seq<ReqlExpr -> ReqlExpr> {
|
||||
match search.continentId with
|
||||
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId)))
|
||||
| None -> ()
|
||||
match srch.remoteWork with
|
||||
match search.remoteWork with
|
||||
| "" -> ()
|
||||
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
||||
match srch.skill with
|
||||
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes")))
|
||||
match search.skill with
|
||||
| Some skl ->
|
||||
yield (fun q -> q.Filter (ReqlFunction1(fun it ->
|
||||
upcast it.G("skills").Contains (ReqlFunction1(fun s ->
|
||||
upcast s.G("description").Match (regexContains skl))))) :> ReqlExpr)
|
||||
it.G("skills").Contains (ReqlFunction1(fun s -> s.G("description").Match (regexContains skl))))))
|
||||
| None -> ()
|
||||
match srch.bioExperience with
|
||||
match search.bioExperience with
|
||||
| Some text ->
|
||||
let txt = regexContains text
|
||||
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 -> ()
|
||||
}
|
||||
|> Seq.toList
|
||||
|
@ -300,31 +296,26 @@ module Profile =
|
|||
.With ("citizenId", it.G "id")))
|
||||
.Pluck("citizenId", "displayName", "seekingEmployment", "remoteWork", "fullTime", "lastUpdatedOn")
|
||||
.OrderBy(ReqlFunction1 (fun it -> upcast it.G("displayName").Downcase ()))
|
||||
.RunResultAsync<ProfileSearchResult list> c
|
||||
.RunResultAsync<ProfileSearchResult list>
|
||||
|> withReconn conn
|
||||
|
||||
// Search profiles (public)
|
||||
let publicSearch (srch : PublicSearch) conn =
|
||||
fun c ->
|
||||
(seq {
|
||||
(seq<ReqlExpr -> ReqlExpr> {
|
||||
match srch.continentId with
|
||||
| Some conId ->
|
||||
yield (fun (q : ReqlExpr) ->
|
||||
q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
|
||||
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString cId)))
|
||||
| None -> ()
|
||||
match srch.region with
|
||||
| Some reg ->
|
||||
yield (fun q ->
|
||||
q.Filter (ReqlFunction1 (fun it -> upcast it.G("region").Match (regexContains reg))) :> ReqlExpr)
|
||||
yield (fun q -> q.Filter (ReqlFunction1 (fun it -> upcast it.G("region").Match (regexContains reg))))
|
||||
| None -> ()
|
||||
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
|
||||
| Some skl ->
|
||||
yield (fun q -> q.Filter (ReqlFunction1 (fun it ->
|
||||
upcast it.G("skills").Contains (ReqlFunction1(fun s ->
|
||||
upcast s.G("description").Match (regexContains skl))))) :> ReqlExpr)
|
||||
it.G("skills").Contains (ReqlFunction1(fun s -> s.G("description").Match (regexContains skl))))))
|
||||
| None -> ()
|
||||
}
|
||||
|> Seq.toList
|
||||
|
@ -334,7 +325,7 @@ module Profile =
|
|||
.EqJoin("continentId", r.Table Table.Continent)
|
||||
.Without(r.HashMap ("right", "id"))
|
||||
.Zip()
|
||||
.Filter(r.HashMap ("isPublic", true)) :> ReqlExpr))
|
||||
.Filter(r.HashMap ("isPublic", true))))
|
||||
.Merge(ReqlFunction1 (fun it ->
|
||||
upcast r
|
||||
.HashMap("skills",
|
||||
|
@ -343,7 +334,7 @@ module Profile =
|
|||
skill.G("description").Add(" (").Add(skill.G("notes")).Add ")"))))
|
||||
.With("continent", it.G "name")))
|
||||
.Pluck("continent", "region", "skills", "remoteWork")
|
||||
.RunResultAsync<PublicSearchResult list> c
|
||||
.RunResultAsync<PublicSearchResult list>
|
||||
|> withReconn conn
|
||||
|
||||
/// Citizen data access functions
|
||||
|
@ -445,7 +436,7 @@ module Listing =
|
|||
r.Table(Table.Listing)
|
||||
.GetAll(citizenId).OptArg("index", nameof citizenId)
|
||||
.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>
|
||||
|> withReconn conn
|
||||
|
||||
|
@ -463,7 +454,7 @@ module Listing =
|
|||
r.Table(Table.Listing)
|
||||
.Filter(r.HashMap ("id", listingId))
|
||||
.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
|
||||
return List.tryHead listing
|
||||
}
|
||||
|
@ -493,39 +484,33 @@ module Listing =
|
|||
|> withReconnIgnore conn
|
||||
|
||||
/// Search job listings
|
||||
let search (srch : ListingSearch) conn =
|
||||
fun c ->
|
||||
(seq {
|
||||
match srch.continentId with
|
||||
| Some conId ->
|
||||
yield (fun (q : ReqlExpr) ->
|
||||
q.Filter (r.HashMap (nameof srch.continentId, ContinentId.ofString conId)) :> ReqlExpr)
|
||||
let search (search : ListingSearch) conn =
|
||||
(seq<ReqlExpr -> ReqlExpr> {
|
||||
match search.continentId with
|
||||
| Some cId -> yield (fun q -> q.Filter (r.HashMap (nameof search.continentId, ContinentId.ofString cId)))
|
||||
| None -> ()
|
||||
match srch.region with
|
||||
match search.region with
|
||||
| Some rgn ->
|
||||
yield (fun q ->
|
||||
q.Filter (ReqlFunction1 (fun it ->
|
||||
upcast it.G(nameof srch.region).Match (regexContains rgn))) :> ReqlExpr)
|
||||
q.Filter (ReqlFunction1 (fun it -> it.G(nameof search.region).Match (regexContains rgn))))
|
||||
| None -> ()
|
||||
match srch.remoteWork with
|
||||
match search.remoteWork with
|
||||
| "" -> ()
|
||||
| _ ->
|
||||
yield (fun q -> q.Filter (r.HashMap (nameof srch.remoteWork, srch.remoteWork = "yes")) :> ReqlExpr)
|
||||
match srch.text with
|
||||
| _ -> yield (fun q -> q.Filter (r.HashMap (nameof search.remoteWork, search.remoteWork = "yes")))
|
||||
match search.text with
|
||||
| Some text ->
|
||||
yield (fun q ->
|
||||
q.Filter (ReqlFunction1 (fun it ->
|
||||
upcast it.G(nameof srch.text).Match (regexContains text))) :> ReqlExpr)
|
||||
q.Filter (ReqlFunction1 (fun it -> it.G(nameof search.text).Match (regexContains text))))
|
||||
| None -> ()
|
||||
}
|
||||
|> Seq.toList
|
||||
|> List.fold
|
||||
(fun q f -> f q)
|
||||
(r.Table(Table.Listing)
|
||||
.GetAll(false).OptArg ("index", "isExpired") :> ReqlExpr))
|
||||
.GetAll(false).OptArg ("index", "isExpired")))
|
||||
.EqJoin("continentId", r.Table Table.Continent)
|
||||
.Map(ReqlFunction1 (fun it -> upcast r.HashMap("listing", it.G "left").With ("continent", it.G "right")))
|
||||
.RunResultAsync<ListingForView list> c
|
||||
.Map(ReqlFunction1 (fun it -> r.HashMap("listing", it.G "left").With ("continent", it.G "right")))
|
||||
.RunResultAsync<ListingForView list>
|
||||
|> withReconn conn
|
||||
|
||||
|
||||
|
@ -555,8 +540,7 @@ module Success =
|
|||
.Without(r.HashMap ("right", "id"))
|
||||
.Zip()
|
||||
.Merge(ReqlFunction1 (fun it ->
|
||||
upcast r
|
||||
.HashMap("citizenName",
|
||||
r.HashMap("citizenName",
|
||||
r.Branch(it.G("realName" ).Default_("").Ne "", it.G "realName",
|
||||
it.G("displayName").Default_("").Ne "", it.G "displayName",
|
||||
it.G "mastodonUser"))
|
||||
|
|
|
@ -21,14 +21,13 @@ module Error =
|
|||
open System.Threading.Tasks
|
||||
|
||||
/// URL prefixes for the Vue app
|
||||
let vueUrls = [
|
||||
"/how-it-works"; "/privacy-policy"; "/terms-of-service"; "/citizen"; "/help-wanted"; "/listing"; "/profile"
|
||||
let vueUrls =
|
||||
[ "/how-it-works"; "/privacy-policy"; "/terms-of-service"; "/citizen"; "/help-wanted"; "/listing"; "/profile"
|
||||
"/so-long"; "/success-story"
|
||||
]
|
||||
|
||||
/// Handler that will return a status code 404 and the text "Not Found"
|
||||
let notFound : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
let notFound : HttpHandler = fun next ctx -> task {
|
||||
let fac = ctx.GetService<ILoggerFactory> ()
|
||||
let log = fac.CreateLogger "Handler"
|
||||
let path = string ctx.Request.Path
|
||||
|
@ -107,8 +106,7 @@ module Helpers =
|
|||
module Citizen =
|
||||
|
||||
// GET: /api/citizen/log-on/[code]
|
||||
let logOn (abbr, authCode) : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
let logOn (abbr, authCode) : HttpHandler = fun next ctx -> task {
|
||||
// Step 1 - Verify with Mastodon
|
||||
let cfg = authConfig ctx
|
||||
|
||||
|
@ -154,18 +152,14 @@ module Citizen =
|
|||
}
|
||||
|
||||
// GET: /api/citizen/[id]
|
||||
let get citizenId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let get citizenId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Data.Citizen.findById (CitizenId citizenId) (conn ctx) with
|
||||
| Some citizen -> return! json citizen next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// DELETE: /api/citizen
|
||||
let delete : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let delete : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
do! Data.Citizen.delete (currentCitizenId ctx) (conn ctx)
|
||||
return! ok next ctx
|
||||
}
|
||||
|
@ -176,8 +170,7 @@ module Citizen =
|
|||
module Continent =
|
||||
|
||||
// GET: /api/continent/all
|
||||
let all : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
let all : HttpHandler = fun next ctx -> task {
|
||||
let! continents = Data.Continent.all (conn ctx)
|
||||
return! json continents next ctx
|
||||
}
|
||||
|
@ -187,7 +180,7 @@ module Continent =
|
|||
[<RequireQualifiedAccess>]
|
||||
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) =
|
||||
{ name = inst.Name
|
||||
url = inst.Url
|
||||
|
@ -196,8 +189,7 @@ module Instances =
|
|||
}
|
||||
|
||||
// GET: /api/instances
|
||||
let all : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
let all : HttpHandler = fun next ctx -> task {
|
||||
return! json ((authConfig ctx).Instances |> Array.map toInstance) next ctx
|
||||
}
|
||||
|
||||
|
@ -213,35 +205,27 @@ module Listing =
|
|||
let private parseDate = DateTime.Parse >> LocalDate.FromDateTime
|
||||
|
||||
// GET: /api/listings/mine
|
||||
let mine : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let mine : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let! listings = Data.Listing.findByCitizen (currentCitizenId ctx) (conn ctx)
|
||||
return! json listings next ctx
|
||||
}
|
||||
|
||||
// GET: /api/listing/[id]
|
||||
let get listingId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let get listingId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Data.Listing.findById (ListingId listingId) (conn ctx) with
|
||||
| Some listing -> return! json listing next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET: /api/listing/view/[id]
|
||||
let view listingId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let view listingId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Data.Listing.findByIdForView (ListingId listingId) (conn ctx) with
|
||||
| Some listing -> return! json listing next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST: /listings
|
||||
let add : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let add : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let! form = ctx.BindJsonAsync<ListingForm> ()
|
||||
let now = (clock ctx).GetCurrentInstant ()
|
||||
do! Data.Listing.add
|
||||
|
@ -262,9 +246,7 @@ module Listing =
|
|||
}
|
||||
|
||||
// PUT: /api/listing/[id]
|
||||
let update listingId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let update listingId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let dbConn = conn ctx
|
||||
match! Data.Listing.findById (ListingId listingId) dbConn with
|
||||
| Some listing when listing.citizenId <> (currentCitizenId ctx) -> return! Error.notAuthorized next ctx
|
||||
|
@ -285,9 +267,7 @@ module Listing =
|
|||
}
|
||||
|
||||
// PATCH: /api/listing/[id]
|
||||
let expire listingId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> FSharp.Control.Tasks.Affine.task {
|
||||
let expire listingId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let dbConn = conn ctx
|
||||
let now = clock(ctx).GetCurrentInstant ()
|
||||
match! Data.Listing.findById (ListingId listingId) dbConn with
|
||||
|
@ -311,9 +291,7 @@ module Listing =
|
|||
}
|
||||
|
||||
// GET: /api/listing/search
|
||||
let search : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let search : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let search = ctx.BindQueryString<ListingSearch> ()
|
||||
let! results = Data.Listing.search search (conn ctx)
|
||||
return! json results next ctx
|
||||
|
@ -327,27 +305,21 @@ module 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
|
||||
// is not an error). The "get" handler returns a 404 if a profile is not found.
|
||||
let current : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let current : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Data.Profile.findById (currentCitizenId ctx) (conn ctx) with
|
||||
| Some profile -> return! json profile next ctx
|
||||
| None -> return! Successful.NO_CONTENT next ctx
|
||||
}
|
||||
|
||||
// GET: /api/profile/get/[id]
|
||||
let get citizenId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let get citizenId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Data.Profile.findById (CitizenId citizenId) (conn ctx) with
|
||||
| Some profile -> return! json profile next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET: /api/profile/view/[id]
|
||||
let view citizenId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let view citizenId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let citId = CitizenId citizenId
|
||||
let dbConn = conn ctx
|
||||
match! Data.Profile.findById citId dbConn with
|
||||
|
@ -357,8 +329,8 @@ module Profile =
|
|||
match! Data.Continent.findById profile.continentId dbConn with
|
||||
| Some continent ->
|
||||
return!
|
||||
json {
|
||||
profile = profile
|
||||
json
|
||||
{ profile = profile
|
||||
citizen = citizen
|
||||
continent = continent
|
||||
} next ctx
|
||||
|
@ -368,17 +340,13 @@ module Profile =
|
|||
}
|
||||
|
||||
// GET: /api/profile/count
|
||||
let count : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let count : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let! theCount = Data.Profile.count (conn ctx)
|
||||
return! json { count = theCount } next ctx
|
||||
}
|
||||
|
||||
// POST: /api/profile/save
|
||||
let save : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let save : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let citizenId = currentCitizenId ctx
|
||||
let dbConn = conn ctx
|
||||
let! form = ctx.BindJsonAsync<ProfileForm>()
|
||||
|
@ -412,9 +380,7 @@ module Profile =
|
|||
}
|
||||
|
||||
// PATCH: /api/profile/employment-found
|
||||
let employmentFound : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let employmentFound : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let dbConn = conn ctx
|
||||
match! Data.Profile.findById (currentCitizenId ctx) dbConn with
|
||||
| Some profile ->
|
||||
|
@ -424,25 +390,20 @@ module Profile =
|
|||
}
|
||||
|
||||
// DELETE: /api/profile
|
||||
let delete : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let delete : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
do! Data.Profile.delete (currentCitizenId ctx) (conn ctx)
|
||||
return! ok next ctx
|
||||
}
|
||||
|
||||
// GET: /api/profile/search
|
||||
let search : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let search : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let search = ctx.BindQueryString<ProfileSearch> ()
|
||||
let! results = Data.Profile.search search (conn ctx)
|
||||
return! json results next ctx
|
||||
}
|
||||
|
||||
// GET: /api/profile/public-search
|
||||
let publicSearch : HttpHandler =
|
||||
fun next ctx -> task {
|
||||
let publicSearch : HttpHandler = fun next ctx -> task {
|
||||
let search = ctx.BindQueryString<PublicSearch> ()
|
||||
let! results = Data.Profile.publicSearch search (conn ctx)
|
||||
return! json results next ctx
|
||||
|
@ -456,26 +417,20 @@ module Success =
|
|||
open System
|
||||
|
||||
// GET: /api/success/[id]
|
||||
let get successId : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let get successId : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
match! Data.Success.findById (SuccessId successId) (conn ctx) with
|
||||
| Some story -> return! json story next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// GET: /api/success/list
|
||||
let all : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let all : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let! stories = Data.Success.all (conn ctx)
|
||||
return! json stories next ctx
|
||||
}
|
||||
|
||||
// POST: /api/success/save
|
||||
let save : HttpHandler =
|
||||
authorize
|
||||
>=> fun next ctx -> task {
|
||||
let save : HttpHandler = authorize >=> fun next ctx -> task {
|
||||
let citizenId = currentCitizenId ctx
|
||||
let dbConn = conn ctx
|
||||
let now = (clock ctx).GetCurrentInstant ()
|
||||
|
@ -528,15 +483,9 @@ let allEndpoints = [
|
|||
routef "/%O/view" Listing.view
|
||||
route "s/mine" Listing.mine
|
||||
]
|
||||
PATCH [
|
||||
routef "/%O" Listing.expire
|
||||
]
|
||||
POST [
|
||||
route "s" Listing.add
|
||||
]
|
||||
PUT [
|
||||
routef "/%O" Listing.update
|
||||
]
|
||||
PATCH [ routef "/%O" Listing.expire ]
|
||||
POST [ route "s" Listing.add ]
|
||||
PUT [ routef "/%O" Listing.update ]
|
||||
]
|
||||
subRoute "/profile" [
|
||||
GET_HEAD [
|
||||
|
@ -558,4 +507,4 @@ let allEndpoints = [
|
|||
POST [ route "" Success.save ]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue
Block a user