117 lines
4.4 KiB
Forth

module CurrencyAPI.App
open System
open System.IO
open Microsoft.AspNetCore.Builder
open Microsoft.AspNetCore.Hosting
open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Hosting
open Microsoft.Extensions.Logging
open Microsoft.Extensions.DependencyInjection
open Giraffe
open dotenv.net
open Npgsql.FSharp
let prodEnv = DotEnv.Read(DotEnvOptions(envFilePaths = [ "../.prod.env"], overwriteExistingVars = false))
let ( _ , connStr ) = prodEnv.TryGetValue("DATABASE_URL")
let ( _ , apiKey ) = prodEnv.TryGetValue("API_KEY")
let validateApiKey (ctx : HttpContext) =
match ctx.TryGetRequestHeader "X-API-Key" with
| Some key -> apiKey.Equals key
| None -> false
let accessDenied = setStatusCode 401 >=> text "Access Denied"
let requiresApiKey = authorizeRequest validateApiKey accessDenied
let getCurrentBalance (discordId : string) =
task {
let! amounts =
connStr
|> Sql.connect
|> Sql.parameters [ "did" , Sql.string discordId ]
|> Sql.query """SELECT gbt FROM "user" WHERE discord_id = @did"""
|> Sql.executeAsync (fun r -> r.int "gbt")
match amounts with
| [] -> return Error "User not found"
| a::_ -> return Ok a
}
let get (discordId : string) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
try
match! getCurrentBalance discordId with
| Ok amount -> return! json {| Amount = amount |} next ctx
| Error e -> return! RequestErrors.notFound (json {| Error = e |}) next ctx
with ex ->
return! ServerErrors.internalError (json {| Error = ex.Message |}) next ctx
}
let modify sign (discordId : string) : HttpHandler =
fun (next : HttpFunc) (ctx : HttpContext) ->
task {
let! body = ctx.BindJsonAsync<{|Amount:int|}>()
match! getCurrentBalance discordId with
| Ok current ->
let amount = body.Amount * sign
if current + amount < 0 then
return! RequestErrors.badRequest (json {| Error = "Insufficient funds" |}) next ctx
else
try
let! _ =
connStr
|> Sql.connect
|> Sql.parameters [ "did" , Sql.string discordId ; "amount" , Sql.int amount ]
|> Sql.query """UPDATE "user" SET gbt = GREATEST(gbt + @amount, 0) WHERE discord_id = @did"""
|> Sql.executeNonQueryAsync
return! json {| NewBalance = current + amount |} next ctx
with ex -> return! RequestErrors.notFound (json {| Error = ex.Message |}) next ctx
| Error e -> return! RequestErrors.notFound (json {| Error = e |}) next ctx
}
let webApp =
choose [
GET >=> requiresApiKey >=> routef "/user/%s/balance" get
PATCH >=> requiresApiKey >=> routef "/user/%s/balance/withdraw" (modify -1)
PATCH >=> requiresApiKey >=> routef "/user/%s/balance/deposit" (modify +1)
RequestErrors.NOT_FOUND "Not Found" ]
let errorHandler (ex : Exception) (logger : ILogger) =
logger.LogError(ex, "An unhandled exception has occurred while executing the request.")
clearResponse >=> setStatusCode 500 >=> text ex.Message
let configureApp (app : IApplicationBuilder) =
let env = app.ApplicationServices.GetService<IWebHostEnvironment>()
if env.IsDevelopment() then
app.UseDeveloperExceptionPage()
else
app.UseGiraffeErrorHandler(errorHandler)
|> ignore
app.UseGiraffe(webApp)
let configureServices (services : IServiceCollection) =
services.AddGiraffe() |> ignore
let configureLogging (builder : ILoggingBuilder) =
builder.AddConsole()
.AddDebug() |> ignore
[<EntryPoint>]
let main args =
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(
fun webHostBuilder ->
webHostBuilder
.ConfigureKestrel(fun opt ->
opt.AddServerHeader <- false
opt.ListenLocalhost(3333, (fun o -> o.UseHttps() |> ignore)))
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
|> ignore)
.Build()
.Run()
0