summaryrefslogtreecommitdiff
path: root/src/FundLab.Api
diff options
context:
space:
mode:
authorSomhairle H. Marisol <[email protected]>2026-09-20 22:59:00 +0800
committerSomhairle H. Marisol <[email protected]>2026-09-20 22:59:00 +0800
commitc60905e9e7f992a7f8c79c3812e92a44b2d606f5 (patch)
treea895bc6f0ebe669407ffe4d1a11439e03a6fe0d4 /src/FundLab.Api
downloadfund-lab-c60905e9e7f992a7f8c79c3812e92a44b2d606f5.tar.gz
feat(core): 建立 fund-lab 可运行基线
[变更性质] - 本提交冻结当前可构建、可测试的应用基线,不包含 PostgreSQL 持久化。 [新增功能] - 建立 F# Domain、API、Worker、Web 及测试项目。 - 增加 Bearer 认证、健康检查、账本领域模型和中文空状态页面。 [实现方案] - 使用环境变量模板注入认证配置,并排除数据、凭证和构建产物。 - 保留 19 个 Domain 测试和 5 个 API 测试作为后续变更基准。 [影响范围] - 为后续 3a PostgreSQL FOF 创建/读取切片提供可回滚基线。 - 当前仍不接入真实基金数据、真实交易或数据库。
Diffstat (limited to 'src/FundLab.Api')
-rw-r--r--src/FundLab.Api/App.fs53
-rw-r--r--src/FundLab.Api/Authentication.fs25
-rw-r--r--src/FundLab.Api/FundLab.Api.fsproj20
-rw-r--r--src/FundLab.Api/Health.fs16
-rw-r--r--src/FundLab.Api/Program.fs15
5 files changed, 129 insertions, 0 deletions
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 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+ <PropertyGroup>
+ <TargetFramework>net8.0</TargetFramework>
+ <GenerateDocumentationFile>true</GenerateDocumentationFile>
+ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
+ <Nullable>enable</Nullable>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="Giraffe" Version="6.0.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\FundLab.Domain\FundLab.Domain.fsproj" />
+ </ItemGroup>
+ <ItemGroup>
+ <Compile Include="Authentication.fs" />
+ <Compile Include="Health.fs" />
+ <Compile Include="App.fs" />
+ <Compile Include="Program.fs" />
+ </ItemGroup>
+</Project>
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<HealthResponse> {
+ 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
+
+[<EntryPoint>]
+let main argv =
+ let builder = WebApplication.CreateBuilder(argv)
+ builder.Services.AddGiraffe() |> ignore
+
+ let app = builder.Build()
+ app.UseGiraffe(App.createApplication())
+ app.Run()
+ 0