From c60905e9e7f992a7f8c79c3812e92a44b2d606f5 Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Sun, 20 Sep 2026 22:59:00 +0800 Subject: feat(core): 建立 fund-lab 可运行基线 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [变更性质] - 本提交冻结当前可构建、可测试的应用基线,不包含 PostgreSQL 持久化。 [新增功能] - 建立 F# Domain、API、Worker、Web 及测试项目。 - 增加 Bearer 认证、健康检查、账本领域模型和中文空状态页面。 [实现方案] - 使用环境变量模板注入认证配置,并排除数据、凭证和构建产物。 - 保留 19 个 Domain 测试和 5 个 API 测试作为后续变更基准。 [影响范围] - 为后续 3a PostgreSQL FOF 创建/读取切片提供可回滚基线。 - 当前仍不接入真实基金数据、真实交易或数据库。 --- src/FundLab.Api/App.fs | 53 ++++++++++++++++++++++++++++++++++++++ src/FundLab.Api/Authentication.fs | 25 ++++++++++++++++++ src/FundLab.Api/FundLab.Api.fsproj | 20 ++++++++++++++ src/FundLab.Api/Health.fs | 16 ++++++++++++ src/FundLab.Api/Program.fs | 15 +++++++++++ 5 files changed, 129 insertions(+) create mode 100644 src/FundLab.Api/App.fs create mode 100644 src/FundLab.Api/Authentication.fs create mode 100644 src/FundLab.Api/FundLab.Api.fsproj create mode 100644 src/FundLab.Api/Health.fs create mode 100644 src/FundLab.Api/Program.fs (limited to 'src/FundLab.Api') diff --git a/src/FundLab.Api/App.fs b/src/FundLab.Api/App.fs new file mode 100644 index 0000000..44628ab --- /dev/null +++ b/src/FundLab.Api/App.fs @@ -0,0 +1,53 @@ +namespace FundLab.Api + +open System +open Giraffe +open Microsoft.AspNetCore.Http + +type EmptyPortfolioResponse = + { + status: string + message: string + } + +module App = + let private unauthorized : HttpHandler = + setStatusCode 401 + >=> setHttpHeader "WWW-Authenticate" "Bearer" + >=> text "Unauthorized" + + let private requireBearer (next: HttpFunc) (ctx: HttpContext) = + let expectedToken = + Environment.GetEnvironmentVariable("FUND_LAB_AUTH_TOKEN") + |> Option.ofObj + |> Option.defaultValue "" + + let authorizationHeader = ctx.Request.Headers.Authorization.ToString() + + match Authentication.authorize expectedToken authorizationHeader with + | AuthDecision.Authorized -> next ctx + | AuthDecision.Unauthorized -> unauthorized next ctx + + let private health : HttpHandler = + json ({ + service = "fund-lab-api" + status = "ok" + } : HealthResponse) + + let private emptyPortfolio : HttpHandler = + json { + status = "empty" + message = "尚未创建基金/尚未选择投资" + } + + let createApplication () : HttpHandler = + choose [ + GET >=> route "/health" >=> health + subRoute "/api" ( + requireBearer + >=> choose [ + GET >=> route "/portfolio/summary" >=> emptyPortfolio + ] + ) + setStatusCode 404 >=> text "Not Found" + ] diff --git a/src/FundLab.Api/Authentication.fs b/src/FundLab.Api/Authentication.fs new file mode 100644 index 0000000..e98d71e --- /dev/null +++ b/src/FundLab.Api/Authentication.fs @@ -0,0 +1,25 @@ +namespace FundLab.Api + +open System +open System.Security.Cryptography +open System.Text + +type AuthDecision = + | Authorized + | Unauthorized + +module Authentication = + let authorize (expectedToken: string) (authorizationHeader: string) : AuthDecision = + if String.IsNullOrWhiteSpace expectedToken + || String.IsNullOrWhiteSpace authorizationHeader + || not (authorizationHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) then + AuthDecision.Unauthorized + else + let presentedToken = authorizationHeader.Substring("Bearer ".Length) + let expectedBytes = Encoding.UTF8.GetBytes expectedToken + let presentedBytes = Encoding.UTF8.GetBytes presentedToken + + if CryptographicOperations.FixedTimeEquals(expectedBytes, presentedBytes) then + AuthDecision.Authorized + else + AuthDecision.Unauthorized diff --git a/src/FundLab.Api/FundLab.Api.fsproj b/src/FundLab.Api/FundLab.Api.fsproj new file mode 100644 index 0000000..81952df --- /dev/null +++ b/src/FundLab.Api/FundLab.Api.fsproj @@ -0,0 +1,20 @@ + + + net8.0 + true + true + enable + + + + + + + + + + + + + + diff --git a/src/FundLab.Api/Health.fs b/src/FundLab.Api/Health.fs new file mode 100644 index 0000000..719446f --- /dev/null +++ b/src/FundLab.Api/Health.fs @@ -0,0 +1,16 @@ +namespace FundLab.Api + +open System.Text.Json + +type HealthResponse = + { + service: string + status: string + } + +module Health = + let payload (serviceName: string) : string = + JsonSerializer.Serialize { + service = serviceName + status = "ok" + } diff --git a/src/FundLab.Api/Program.fs b/src/FundLab.Api/Program.fs new file mode 100644 index 0000000..d5cc0b2 --- /dev/null +++ b/src/FundLab.Api/Program.fs @@ -0,0 +1,15 @@ +module FundLab.Api.Program + +open Giraffe +open Microsoft.AspNetCore.Builder +open Microsoft.Extensions.DependencyInjection + +[] +let main argv = + let builder = WebApplication.CreateBuilder(argv) + builder.Services.AddGiraffe() |> ignore + + let app = builder.Build() + app.UseGiraffe(App.createApplication()) + app.Run() + 0 -- cgit v1.2.3