148 lines
4.6 KiB
Forth

module JobsJobsJobs.Api.Handlers
open Data
open Domain
open FSharp.Json
open Suave
open Suave.Operators
open System
[<AutoOpen>]
module private Internal =
open Suave.Writers
/// Send a JSON response
let json x =
Successful.OK (Json.serialize x)
>=> setMimeType "application/json; charset=utf-8"
module Auth =
open System.Net.Http
open System.Net.Http.Headers
/// Verify a user's credentials with No Agenda Social
let verifyWithMastodon accessToken = async {
use client = new HttpClient ()
use req = new HttpRequestMessage (HttpMethod.Get, (sprintf "%saccounts/verify_credentials" config.auth.apiUrl))
req.Headers.Authorization <- AuthenticationHeaderValue <| sprintf "Bearer %s" accessToken
match! client.SendAsync req |> Async.AwaitTask with
| res when res.IsSuccessStatusCode ->
let! body = res.Content.ReadAsStringAsync ()
return
match Json.deserialize<ViewModels.Citizen.MastodonAccount> body with
| profile when profile.username = profile.acct -> Ok profile
| profile -> Error (sprintf "Profiles must be from noagendasocial.com; yours is %s" profile.acct)
| res -> return Error (sprintf "Could not retrieve credentials: %d ~ %s" (int res.StatusCode) res.ReasonPhrase)
}
/// Handler to return the Vue application
module Vue =
/// The application index page
let app = Files.file "wwwroot/index.html"
/// Handlers for error conditions
module Error =
open Suave.Logging
open Suave.Logging.Message
/// Handle errors
let error (ex : Exception) msg =
fun ctx ->
seq {
string ctx.request.url
match msg with "" -> () | _ -> " ~ "; msg
"\n"; (ex.GetType().Name); ": "; ex.Message; "\n"
ex.StackTrace
}
|> Seq.reduce (+)
|> (eventX >> ctx.runtime.logger.error)
ServerErrors.INTERNAL_ERROR (Json.serialize {| error = ex.Message |}) ctx
/// Handle 404s from the API, sending known URL paths to the Vue app so that they can be handled there
let notFound : WebPart =
fun ctx ->
[ "/user"; "/jobs" ]
|> List.filter ctx.request.path.StartsWith
|> List.length
|> function
| 0 -> RequestErrors.NOT_FOUND "err" ctx
| _ -> Vue.app ctx
/// /api/citizen route handlers
module Citizen =
open ViewModels.Citizen
/// Either add the user, or update their display name and last seen date
let establishCitizen result profile = async {
match result with
| Some citId ->
match! Citizens.update citId profile.displayName with
| Ok _ -> return Ok citId
| Error exn -> return Error exn
| None ->
let now = DateTime.Now.toMillis ()
let! citId = CitizenId.create ()
match! Citizens.add
{ id = citId
naUser = profile.username
displayName = profile.displayName
profileUrl = profile.url
joinedOn = now
lastSeenOn = now
} with
| Ok _ -> return Ok citId
| Error exn -> return Error exn
}
/// POST: /api/citizen/log-on
let logOn : WebPart =
fun ctx -> async {
match ctx.fromJsonBody<LogOn> () with
| Ok data ->
match! Auth.verifyWithMastodon data.accessToken with
| Ok profile ->
match! Citizens.findIdByNaUser profile.username with
| Ok idResult ->
match! establishCitizen idResult profile with
| Ok citizenId ->
// TODO: replace this with a JWT issued by the server user
match! Citizens.tryFind citizenId with
| Ok (Some citizen) -> return! json citizen ctx
| Ok None -> return! Error.error (exn ()) "Citizen record not found" ctx
| Error exn -> return! Error.error exn "Could not retrieve user from database" ctx
| Error exn -> return! Error.error exn "Could not update Jobs, Jobs, Jobs database" ctx
| Error exn -> return! Error.error exn "Token not received" ctx
| Error msg ->
// Error message regarding exclusivity to No Agenda Social members
return Some ctx
| Error exn -> return! Error.error exn "Token not received" ctx
}
open Suave.Filters
/// The routes for Jobs, Jobs, Jobs
let webApp =
choose
[ GET >=> choose
[ path "/" >=> Vue.app
Files.browse "wwwroot/"
]
// PUT >=> choose
// [ ]
// PATCH >=> choose
// [ ]
POST >=> choose
[ path "/api/citizen/log-on" >=> Citizen.logOn
]
Error.notFound
]