118 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
DotEnv.Load(DotEnvOptions(envFilePaths = [ "../.prod.env" ], overwriteExistingVars = false))
let connStr = Environment.GetEnvironmentVariable("DATABASE_URL")
.Replace("postgresql://", "postgres://")
.Replace("?sslmode=require", "")
let apiKey = Environment.GetEnvironmentVariable("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)
.Configure(Action<IApplicationBuilder> configureApp)
.ConfigureServices(configureServices)
.ConfigureLogging(configureLogging)
|> ignore)
.Build()
.Run()
0