fix: align backend APIs and upload flow

This commit is contained in:
2026-09-15 14:10:55 +08:00
parent 32177a7293
commit 53e6195938
149 changed files with 4791 additions and 435 deletions
+39
View File
@@ -0,0 +1,39 @@
param([switch]$Docker)
$ErrorActionPreference = 'Stop'
& (Join-Path $PSScriptRoot 'Initialize-Local.ps1')
$secrets = @{}
Get-Content -LiteralPath (Join-Path $PSScriptRoot '.env.local') | ForEach-Object { if ($_ -match '^([^#=]+)=(.*)$') { $secrets[$Matches[1]] = $Matches[2] } }
$serviceMap = @{ admin=@('Admin.WebApi','Admin.WebApi.dll',5180); user=@('User.WebApi','IdentityService.WebApi.dll',5181); contact=@('ContactService.WebApi','ContactService.WebApi.dll',5182); group=@('GroupService.WebApi','GroupService.WebApi.dll',5183); message=@('MessageService.WebApi','MessageService.WebApi.dll',5184); file=@('FileService.WebApi','FileService.WebApi.dll',5185); connector=@('ConnectorService','ConnectorService.dll',5186) }
$hosts = @{}
foreach ($name in $serviceMap.Keys) { $hosts[$name] = if ($Docker) { "http://$($name):8080" } else { "http://127.0.0.1:$($serviceMap[$name][2])" } }
$dbHost = if ($Docker) { 'mysql;Port=3306' } else { '127.0.0.1;Port=13306' }
$redisAddress = if ($Docker) { 'redis:6379' } else { '127.0.0.1:16379' }
$rabbitHost = if ($Docker) { 'rabbitmq' } else { '127.0.0.1' }
$rabbitPort = if ($Docker) { 5672 } else { 15672 }
$consulUrl = if ($Docker) { 'http://consul:8500' } else { 'http://127.0.0.1:18500' }
$storageRoot = if ($Docker) { '/data/files' } else { Join-Path $PSScriptRoot 'data/files' }
$keysRoot = if ($Docker) { '/data/keyring' } else { Join-Path $PSScriptRoot 'data/keyring' }
$cert = [Security.Cryptography.X509Certificates.X509Certificate2]::CreateFromPem([IO.File]::ReadAllText((Join-Path $PSScriptRoot 'data/certs/smtp.crt')))
$smtpPin = $cert.GetCertHashString([Security.Cryptography.HashAlgorithmName]::SHA256)
$cert.Dispose()
$connection = "Server=$dbHost;Database=im_local;User=im_local;Password=$($secrets.MYSQL_PASSWORD);Allow User Variables=true"
$config = @{
ConnectionStrings = @{ DefaultConnection=$connection; Admin=$connection; Redis=$redisAddress }
Jwt = @{ Key=$secrets.JWT_KEY; Issuer='IM.Local'; Audience='IM.Client'; AccessTokenMinutes=30; RefreshTokenDays=7 }
RabbitMQOptions = @{ Host=$rabbitHost; Port=$rabbitPort; Username='im_local'; Password=$secrets.RABBITMQ_PASSWORD; QuequeName='im-local' }
Cors = @{ Origins=@('http://127.0.0.1:5178','http://localhost:5173','http://127.0.0.1:5173','http://localhost:5174') }
GrpcConfigs = @{
IdentityServiceUrl = $(if ($Docker) {'http://user:8081'} else {'http://127.0.0.1:5281'})
ContactServiceUrl = $(if ($Docker) {'http://contact:8081'} else {'http://127.0.0.1:5282'})
MessageServiceUrl = $(if ($Docker) {'http://message:8081'} else {'http://127.0.0.1:5284'})
}
InternalApiKey = $secrets.MANAGEMENT_INTERNAL_KEY
InternalServices = @{ GroupServiceBaseUrl=$hosts.group }
Management = @{ Enabled=$true; InternalKey=$secrets.MANAGEMENT_INTERNAL_KEY; CredentialKey=$secrets.CREDENTIAL_KEY; KeyRingPath=$keysRoot; Services=$hosts; AdminPublicUrl='http://127.0.0.1:5178'; AllowedInfrastructureHosts=@('localhost','127.0.0.1','minio','smtp'); AllowedStorageRoots=@($storageRoot); RabbitHost=$rabbitHost; RabbitPort=$rabbitPort; ConsulUrl=$consulUrl; DevelopmentSmtpCertificateSha256=$smtpPin }
StorageOptions = @{ DefaultProviderCode='Local'; Providers=@{ Local=@{ ProviderCode='Local'; ProviderType=1; Enabled=$true; Bucket='private'; PublicBucket='public'; Region='local'; LocalRootPath=$storageRoot; LocalUploadApiBaseUrl=$hosts.file; PublicBaseUrl=$hosts.file; MaxObjectSizeBytes=1073741824; MinPartSizeBytes=5242880; DefaultPartSizeBytes=5242880; MaxPartCount=10000 } } }
}
$json = $config | ConvertTo-Json -Depth 12
Invoke-RestMethod -Uri 'http://127.0.0.1:18500/v1/kv/IM/Development/appsettings.json' -Method Put -ContentType 'application/json' -Body ([Text.Encoding]::UTF8.GetBytes($json)) | Out-Null
$path = Join-Path $PSScriptRoot 'data/startup.local.json'
[IO.File]::WriteAllText($path,$json)
Write-Output 'Local startup configuration was written to the isolated Consul instance.'
+31
View File
@@ -0,0 +1,31 @@
# Creates only local development secrets and a SMTP test certificate. No default administrator password.
$ErrorActionPreference = 'Stop'
$root = $PSScriptRoot
$envPath = Join-Path $root '.env.local'
if (!(Test-Path -LiteralPath $envPath)) {
function New-Secret { [Convert]::ToHexString([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) }
$entries = @(
('MYSQL_ROOT_PASSWORD=' + (New-Secret))
('MYSQL_PASSWORD=' + (New-Secret))
('RABBITMQ_PASSWORD=' + (New-Secret))
('S3_PASSWORD=' + (New-Secret))
('MANAGEMENT_INTERNAL_KEY=' + (New-Secret))
('JWT_KEY=' + (New-Secret))
('CREDENTIAL_KEY=' + [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)))
)
[IO.File]::WriteAllLines($envPath, $entries)
}
$certRoot = Join-Path $root 'data/certs'
New-Item -ItemType Directory -Force -Path $certRoot | Out-Null
if (!(Test-Path -LiteralPath (Join-Path $certRoot 'smtp.crt'))) {
$rsa = [Security.Cryptography.RSA]::Create(2048)
$request = [Security.Cryptography.X509Certificates.CertificateRequest]::new('CN=localhost', $rsa, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1)
$san = [Security.Cryptography.X509Certificates.SubjectAlternativeNameBuilder]::new()
$san.AddDnsName('localhost'); $san.AddDnsName('smtp'); $san.AddIpAddress([Net.IPAddress]::Loopback)
$request.CertificateExtensions.Add($san.Build())
$cert = $request.CreateSelfSigned([DateTimeOffset]::UtcNow.AddMinutes(-5), [DateTimeOffset]::UtcNow.AddMonths(6))
[IO.File]::WriteAllText((Join-Path $certRoot 'smtp.crt'), $cert.ExportCertificatePem())
[IO.File]::WriteAllText((Join-Path $certRoot 'smtp.key'), $rsa.ExportPkcs8PrivateKeyPem())
$rsa.Dispose(); $cert.Dispose()
}
Write-Output 'Local dependency secrets and SMTP certificate are ready. No administrator account was created.'
+43
View File
@@ -0,0 +1,43 @@
param([switch]$Migrate, [switch]$InitializeAdmin, [switch]$ResetAdmin, [switch]$Start)
$ErrorActionPreference = 'Stop'
$repo = Split-Path $PSScriptRoot
$configPath = Join-Path $PSScriptRoot 'data/startup.local.json'
if (!(Test-Path -LiteralPath $configPath)) { throw 'Run Configure-Local.ps1 first.' }
$config = Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json -AsHashtable
function Set-ConfigEnvironment($node, $prefix = '') {
foreach ($key in $node.Keys) {
$name = if ($prefix) { $prefix + '__' + $key } else { $key }
$value = $node[$key]
if ($value -is [System.Collections.IDictionary]) { Set-ConfigEnvironment $value $name }
elseif ($value -is [array]) { for($i=0;$i -lt $value.Count;$i++) { [Environment]::SetEnvironmentVariable($name+'__'+$i,[string]$value[$i],'Process') } }
else { [Environment]::SetEnvironmentVariable($name,[string]$value,'Process') }
}
}
Set-ConfigEnvironment $config
$env:ASPNETCORE_ENVIRONMENT = 'Development'
$env:CONSUL_URL = 'http://127.0.0.1:18500'
$services = @(@('admin','Admin.WebApi','Admin.WebApi.dll',5180),@('user','User.WebApi','IdentityService.WebApi.dll',5181),@('contact','ContactService.WebApi','ContactService.WebApi.dll',5182),@('group','GroupService.WebApi','GroupService.WebApi.dll',5183),@('message','MessageService.WebApi','MessageService.WebApi.dll',5184),@('file','FileService.WebApi','FileService.WebApi.dll',5185),@('connector','ConnectorService','ConnectorService.dll',5186))
$logs = Join-Path $PSScriptRoot 'data/logs'
New-Item -ItemType Directory -Force -Path $logs | Out-Null
foreach ($service in $services) {
$name,$project,$dll,$port = $service
$env:Management__ServiceName = $name
$env:Kestrel__Endpoints__Http__Url = "http://127.0.0.1:$port"
$env:Kestrel__Endpoints__Http__Protocols = 'Http1'
$env:Kestrel__Endpoints__Grpc__Url = 'http://127.0.0.1:' + ($port+100)
$env:Kestrel__Endpoints__Grpc__Protocols = 'Http2'
$binary = Join-Path $repo "$project/bin/Debug/net8.0/$dll"
if (!(Test-Path -LiteralPath $binary)) { throw "Build $project before starting." }
if ($Migrate -and $name -ne 'connector') { & dotnet $binary --migrate; if ($LASTEXITCODE -ne 0) { throw "Migration failed: $name" } }
if ($name -eq 'admin' -and ($InitializeAdmin -or $ResetAdmin)) {
if (!$env:IM_ADMIN_ACCOUNT -or !$env:IM_ADMIN_PASSWORD) { throw 'Set IM_ADMIN_ACCOUNT and IM_ADMIN_PASSWORD in this process first; no default credentials are created.' }
$action = if ($InitializeAdmin) { '--init-admin' } else { '--reset-admin' }
& dotnet $binary $action; if ($LASTEXITCODE -ne 0) { throw 'Administrator initialization failed.' }
}
if ($Start) {
if (Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue) { throw "Port $port is already in use; refusing to start a duplicate $name service." }
$process = Start-Process -FilePath 'dotnet' -ArgumentList @('"'+$binary+'"') -WorkingDirectory (Split-Path $binary) -WindowStyle Hidden -PassThru -RedirectStandardOutput (Join-Path $logs "$name.out.log") -RedirectStandardError (Join-Path $logs "$name.err.log")
[IO.File]::WriteAllText((Join-Path $logs "$name.pid"),[string]$process.Id)
Write-Output "$name started on port $port (PID $($process.Id))."
}
}
+61
View File
@@ -0,0 +1,61 @@
name: im-admin-local
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Run Initialize-Local.ps1 first}
MYSQL_DATABASE: im_local
MYSQL_USER: im_local
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?Run Initialize-Local.ps1 first}
ports: ["127.0.0.1:13306:3306"]
volumes: ["mysql:/var/lib/mysql"]
healthcheck:
test: ["CMD-SHELL", "MYSQL_PWD=$$MYSQL_PASSWORD mysql -u im_local -e 'SELECT 1' im_local"]
interval: 3s
timeout: 3s
retries: 40
redis:
image: redis:7-alpine
ports: ["127.0.0.1:16379:6379"]
command: redis-server --appendonly yes
volumes: ["redis:/data"]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 3s
timeout: 3s
retries: 20
rabbitmq:
image: rabbitmq:3-management
environment:
RABBITMQ_DEFAULT_USER: im_local
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:?Run Initialize-Local.ps1 first}
ports: ["127.0.0.1:15672:5672", "127.0.0.1:15674:15672"]
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
interval: 5s
timeout: 5s
retries: 30
consul:
image: hashicorp/consul:1.20
command: agent -dev -client=0.0.0.0
ports: ["127.0.0.1:18500:8500"]
minio:
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
command: server /data --console-address :9001
environment:
MINIO_ROOT_USER: im_local
MINIO_ROOT_PASSWORD: ${S3_PASSWORD:?Run Initialize-Local.ps1 first}
ports: ["127.0.0.1:19000:9000", "127.0.0.1:19001:9001"]
volumes: ["minio:/data"]
smtp:
image: axllent/mailpit:v1.27
environment:
MP_SMTP_TLS_CERT: /certs/smtp.crt
MP_SMTP_TLS_KEY: /certs/smtp.key
MP_SMTP_REQUIRE_STARTTLS: "true"
ports: ["127.0.0.1:11025:1025", "127.0.0.1:18025:8025"]
volumes: ["./data/certs:/certs:ro"]
volumes:
mysql:
redis:
minio: