2026-05-16 00:18:22 +08:00
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"os"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type Config struct {
|
|
|
|
|
Addr string
|
|
|
|
|
DatabaseURL string
|
|
|
|
|
// RunWalletAuthSchema: when true, API startup runs CREATE TABLE for wallet_auth_* (use a DDL-capable DB user).
|
|
|
|
|
// Set false on read-only / public API hosts after schema has been applied elsewhere (e.g. admin/migrate host).
|
|
|
|
|
RunWalletAuthSchema bool
|
|
|
|
|
JWTSecret string
|
|
|
|
|
UploadDir string
|
|
|
|
|
UploadMaxMem int64 // passed to ParseMultipartForm (file parts buffer before spilling to disk)
|
|
|
|
|
SeedAdmin bool
|
|
|
|
|
AdminEmail string
|
|
|
|
|
AdminPass string
|
|
|
|
|
CORSOrigins []string
|
|
|
|
|
// S3 (optional). When S3Bucket is set, admin uploads go to S3; use AWS SDK default credentials (env or IAM).
|
|
|
|
|
S3Bucket string
|
|
|
|
|
AWSRegion string
|
|
|
|
|
S3UploadPrefix string
|
|
|
|
|
S3PublicBase string // optional CDN / website endpoint base URL for returned object URLs
|
2026-05-19 07:37:25 +08:00
|
|
|
// S3ObjectACL: optional canned ACL on PutObject (e.g. public-read). Empty = omit ACL (required for Bucket owner enforced buckets).
|
|
|
|
|
S3ObjectACL string
|
2026-05-16 00:18:22 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func Load() Config {
|
|
|
|
|
origins := os.Getenv("CORS_ORIGINS")
|
|
|
|
|
var cors []string
|
|
|
|
|
if origins != "" {
|
|
|
|
|
for _, o := range strings.Split(origins, ",") {
|
|
|
|
|
o = strings.TrimSpace(o)
|
|
|
|
|
if o != "" {
|
|
|
|
|
cors = append(cors, o)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
addr := os.Getenv("HTTP_ADDR")
|
|
|
|
|
if addr == "" {
|
|
|
|
|
addr = ":8080"
|
|
|
|
|
}
|
|
|
|
|
db := os.Getenv("DATABASE_URL")
|
|
|
|
|
if db == "" {
|
|
|
|
|
db = "postgres://ark:ark@localhost:5432/arkdb?sslmode=disable"
|
|
|
|
|
}
|
|
|
|
|
secret := os.Getenv("JWT_SECRET")
|
|
|
|
|
if secret == "" {
|
|
|
|
|
secret = "dev-insecure-change-me"
|
|
|
|
|
}
|
|
|
|
|
upload := os.Getenv("UPLOAD_DIR")
|
|
|
|
|
if upload == "" {
|
|
|
|
|
upload = "./uploads"
|
|
|
|
|
}
|
|
|
|
|
// Memory buffer for multipart file parts (bytes beyond this spill to temp files).
|
|
|
|
|
uploadMem := int64(64 << 20)
|
|
|
|
|
if s := strings.TrimSpace(os.Getenv("UPLOAD_MULTIPART_MEM_MB")); s != "" {
|
|
|
|
|
if mb, err := strconv.ParseInt(s, 10, 64); err == nil && mb > 0 {
|
|
|
|
|
uploadMem = mb * 1 << 20
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
s3Bucket := strings.TrimSpace(os.Getenv("S3_BUCKET"))
|
|
|
|
|
awsRegion := strings.TrimSpace(os.Getenv("AWS_REGION"))
|
|
|
|
|
if awsRegion == "" {
|
|
|
|
|
awsRegion = strings.TrimSpace(os.Getenv("AWS_DEFAULT_REGION"))
|
|
|
|
|
}
|
|
|
|
|
if s3Bucket != "" && awsRegion == "" {
|
|
|
|
|
awsRegion = "ap-southeast-1"
|
|
|
|
|
}
|
|
|
|
|
s3Prefix := strings.TrimSpace(os.Getenv("S3_UPLOAD_PREFIX"))
|
|
|
|
|
if s3Prefix == "" {
|
|
|
|
|
s3Prefix = "uploads"
|
|
|
|
|
}
|
|
|
|
|
s3Prefix = strings.Trim(s3Prefix, "/")
|
|
|
|
|
s3Public := strings.TrimSpace(os.Getenv("S3_PUBLIC_BASE_URL"))
|
2026-05-19 07:37:25 +08:00
|
|
|
s3ObjectACL := strings.TrimSpace(os.Getenv("S3_OBJECT_ACL"))
|
2026-05-16 00:18:22 +08:00
|
|
|
return Config{
|
|
|
|
|
Addr: addr,
|
|
|
|
|
DatabaseURL: db,
|
|
|
|
|
RunWalletAuthSchema: envBoolDefaultTrue("RUN_WALLET_AUTH_SCHEMA"),
|
|
|
|
|
JWTSecret: secret,
|
|
|
|
|
UploadDir: upload,
|
|
|
|
|
UploadMaxMem: uploadMem,
|
|
|
|
|
SeedAdmin: os.Getenv("SEED_ADMIN") == "1" || os.Getenv("SEED_ADMIN") == "true",
|
|
|
|
|
AdminEmail: firstNonEmpty(os.Getenv("ADMIN_EMAIL"), "admin@ark.local"),
|
|
|
|
|
AdminPass: firstNonEmpty(os.Getenv("ADMIN_PASSWORD"), "admin123"),
|
|
|
|
|
CORSOrigins: cors,
|
|
|
|
|
S3Bucket: s3Bucket,
|
|
|
|
|
AWSRegion: awsRegion,
|
|
|
|
|
S3UploadPrefix: s3Prefix,
|
|
|
|
|
S3PublicBase: s3Public,
|
2026-05-19 07:37:25 +08:00
|
|
|
S3ObjectACL: s3ObjectACL,
|
2026-05-16 00:18:22 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// envBoolDefaultTrue returns true when the env var is unset; false for 0/false/no/off (case-insensitive).
|
|
|
|
|
func envBoolDefaultTrue(key string) bool {
|
|
|
|
|
s := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
|
|
|
|
|
if s == "" {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
if s == "0" || s == "false" || s == "no" || s == "off" {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return s == "1" || s == "true" || s == "yes" || s == "on"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func firstNonEmpty(a, b string) string {
|
|
|
|
|
if strings.TrimSpace(a) != "" {
|
|
|
|
|
return a
|
|
|
|
|
}
|
|
|
|
|
return b
|
|
|
|
|
}
|