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() 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 [] let main args = Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults( fun webHostBuilder -> webHostBuilder .ConfigureKestrel(fun opt -> opt.AddServerHeader <- false) .Configure(Action configureApp) .ConfigureServices(configureServices) .ConfigureLogging(configureLogging) |> ignore) .Build() .Run() 0