All checks were successful
build-and-push / build (push) Successful in 32s
- Project.ClientDomain 필드, MailNote(프로젝트 구성원 공동 메모) 모델
- internal/mailsync: 서비스계정+도메인위임으로 팀 메일함을 도메인 검색·집계(stdlib만, push 패턴 재사용)
· GOOGLE_SA_CREDENTIALS_FILE 미설정 시 비활성(graceful)
- GET /projects/{id}/mails (3분 캐시), GET/PUT /projects/{id}/mail-notes
- fix: handlePatchProject map-key Updates가 camelCase 멀티워드 필드(consultingType·
scopeText·pmEmail·clientDomain·날짜)를 컬럼에 못 맞춰 저장 실패하던 버그 → snakeKeys 변환
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
138 lines
4.9 KiB
Go
138 lines
4.9 KiB
Go
package config
|
|
|
|
import (
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Config holds all runtime configuration loaded from environment variables.
|
|
//
|
|
// Variable names follow the cluster/CI contract documented in .env.sample
|
|
// (PGHOST/PGUSER/…, AWS_ACCESS_KEY_ID/…, S3_ENDPOINT/S3_BUCKET/S3_PREFIX) so the
|
|
// same binary runs unchanged in the Special Partners infra. Local docker-compose
|
|
// supplies the same variables. This mirrors the sister eQMS (Mallard) service.
|
|
type Config struct {
|
|
Port string
|
|
DatabaseURL string
|
|
S3Endpoint string
|
|
S3PublicEndpoint string
|
|
S3Region string
|
|
S3Bucket string
|
|
S3Prefix string
|
|
S3AccessKey string
|
|
S3SecretKey string
|
|
DevAuth bool
|
|
SeedData bool
|
|
// FCMCredentialsFile is the path to a Firebase service-account JSON used to
|
|
// send push notifications to spin-mobile. Empty = push disabled (no-op).
|
|
FCMCredentialsFile string
|
|
// GoogleSACredentialsFile is the path to a Google Workspace service-account
|
|
// JSON with DOMAIN-WIDE DELEGATION (gmail.readonly) used to read project
|
|
// client-domain mail. Empty = mail integration disabled.
|
|
GoogleSACredentialsFile string
|
|
// LogoutURL is the common SSO logout path injected by infra (LOGOUT_URL):
|
|
// ends the oauth2-proxy session then redirects to Keycloak end-session.
|
|
// The static frontend reads it via /me. Defaults to the infra-common value.
|
|
LogoutURL string
|
|
// AdminGroups are the Keycloak groups whose members are super-admins
|
|
// (manage everything across the company: incentive console, accounting,
|
|
// approvals, all member/project data).
|
|
AdminGroups []string
|
|
}
|
|
|
|
func env(key, def string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
// firstEnv returns the first non-empty env var among keys, else def.
|
|
func firstEnv(def string, keys ...string) string {
|
|
for _, k := range keys {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
// Load reads configuration from the environment, applying sane local defaults
|
|
// so `go run ./cmd/server` works without docker-compose.
|
|
func Load() Config {
|
|
return Config{
|
|
Port: env("PORT", "8080"),
|
|
DatabaseURL: databaseURL(),
|
|
S3Endpoint: withScheme(env("S3_ENDPOINT", "http://localhost:9000")),
|
|
S3PublicEndpoint: publicEndpoint(),
|
|
S3Region: env("S3_REGION", "us-east-1"),
|
|
S3Bucket: env("S3_BUCKET", "spin"),
|
|
S3Prefix: env("S3_PREFIX", ""),
|
|
// Cluster secrets arrive as AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY;
|
|
// the S3_* aliases are kept for convenience/back-compat.
|
|
S3AccessKey: firstEnv("minioadmin", "AWS_ACCESS_KEY_ID", "S3_ACCESS_KEY"),
|
|
S3SecretKey: firstEnv("minioadmin", "AWS_SECRET_ACCESS_KEY", "S3_SECRET_KEY"),
|
|
DevAuth: env("DEV_AUTH", "true") != "false",
|
|
// Sample data is seeded ONLY when SEED=true is explicitly set. Default is
|
|
// OFF so production never seeds (avoids confusion); local docker-compose /
|
|
// `make be-dev` opt in with SEED=true.
|
|
SeedData: env("SEED", "false") == "true",
|
|
// Super-admin Keycloak groups (comma-separated). Default: admin
|
|
// (shared group name across all internal apps, not app-specific).
|
|
AdminGroups: splitCSV(env("ADMIN_GROUPS", "admin")),
|
|
FCMCredentialsFile: env("FCM_CREDENTIALS_FILE", ""),
|
|
GoogleSACredentialsFile: env("GOOGLE_SA_CREDENTIALS_FILE", ""),
|
|
LogoutURL: env("LOGOUT_URL",
|
|
"/oauth2/sign_out?rd=https%3A%2F%2Fauth.special-partners.com%2Frealms%2Fsp%2Fprotocol%2Fopenid-connect%2Flogout"),
|
|
}
|
|
}
|
|
|
|
// splitCSV splits a comma-separated list, trimming whitespace and dropping
|
|
// empty entries.
|
|
func splitCSV(raw string) []string {
|
|
var out []string
|
|
for _, p := range strings.Split(raw, ",") {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// databaseURL prefers an explicit DATABASE_URL (override), otherwise assembles a
|
|
// DSN from the discrete PG* variables injected from the DB-creds Secret.
|
|
func databaseURL() string {
|
|
if v := os.Getenv("DATABASE_URL"); v != "" {
|
|
return v
|
|
}
|
|
u := url.URL{
|
|
Scheme: "postgres",
|
|
User: url.UserPassword(env("PGUSER", "spin"), env("PGPASSWORD", "spin")),
|
|
Host: env("PGHOST", "localhost") + ":" + env("PGPORT", "5432"),
|
|
Path: "/" + env("PGDATABASE", "spin"),
|
|
}
|
|
q := url.Values{}
|
|
q.Set("sslmode", env("PGSSLMODE", "disable"))
|
|
u.RawQuery = q.Encode()
|
|
return u.String()
|
|
}
|
|
|
|
// publicEndpoint is the browser-reachable S3 host used for presigned URLs.
|
|
// Falls back to the in-cluster endpoint when not separately provided.
|
|
func publicEndpoint() string {
|
|
if v := os.Getenv("S3_PUBLIC_ENDPOINT"); v != "" {
|
|
return withScheme(v)
|
|
}
|
|
return withScheme(env("S3_ENDPOINT", "http://localhost:9000"))
|
|
}
|
|
|
|
// withScheme guarantees an http(s):// prefix so the AWS SDK accepts the endpoint
|
|
// (the .env.sample shows bare hosts like "localhost").
|
|
func withScheme(s string) string {
|
|
if s == "" || strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
|
|
return s
|
|
}
|
|
return "http://" + s
|
|
}
|