Yimaru-BackEnd/internal/services/notification/fcm_credentials_normalize.go
Yared Yemane de8618191c Normalize broken FCM service account JSON (.env PEM newlines).
Repair multiline PEM inside private_key before Firebase init; add unit test; use normalized JSON for credentials.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-20 08:57:26 -07:00

96 lines
2.3 KiB
Go

package notificationservice
import (
"encoding/json"
"strconv"
"strings"
)
// normalizeFCMCredentialsJSON returns credential bytes valid for encoding/json and
// Firebase's credentials parser. Repairs the common mistake of pasting PEM with
// literal newlines inside the JSON "private_key" string (.env multiline breakage).
func normalizeFCMCredentialsJSON(raw string) ([]byte, error) {
s := strings.TrimSpace(strings.TrimPrefix(raw, "\ufeff"))
if json.Valid([]byte(s)) {
return []byte(s), nil
}
if fixed := tryRepairFirebasePrivateKeyNewlines(s); fixed != "" && fixed != s {
b := []byte(fixed)
if json.Valid(b) {
return b, nil
}
}
var m map[string]any
err := json.Unmarshal([]byte(strings.TrimSpace(s)), &m)
return nil, err
}
// tryRepairFirebasePrivateKeyNewlines rewrites `"private_key": "...(PEM with real newlines)..."`
// using strconv.Quote for the inner PEM. Empty string means no rewrite applied.
func tryRepairFirebasePrivateKeyNewlines(s string) string {
const beginRSA = "-----BEGIN RSA PRIVATE KEY-----"
const endRSA = "-----END RSA PRIVATE KEY-----"
const beginPK = "-----BEGIN PRIVATE KEY-----"
const endPK = "-----END PRIVATE KEY-----"
beginIdx := strings.Index(s, beginRSA)
endMarker := endRSA
endIdx := strings.Index(s, endMarker)
if beginIdx < 0 {
beginIdx = strings.Index(s, beginPK)
endMarker = endPK
endIdx = strings.Index(s, endMarker)
}
if beginIdx < 0 || endIdx < beginIdx {
return ""
}
endInclusive := endIdx + len(endMarker)
const pkAttr = `"private_key"`
pkIdx := strings.Index(s, pkAttr)
if pkIdx < 0 || pkIdx > beginIdx {
return ""
}
restAfterKey := s[pkIdx+len(pkAttr):]
colonRel := strings.Index(restAfterKey, ":")
if colonRel < 0 {
return ""
}
searchFrom := pkIdx + len(pkAttr) + colonRel + 1
openQuote := -1
for i := searchFrom; i < beginIdx; i++ {
c := s[i]
if c == '"' {
openQuote = i
break
}
if c != ' ' && c != '\t' && c != '\n' && c != '\r' {
return ""
}
}
if openQuote < 0 {
return ""
}
closeQuote := -1
for i := endInclusive; i < len(s); i++ {
c := s[i]
if c == '"' {
closeQuote = i
break
}
if c != ' ' && c != '\t' && c != '\n' && c != '\r' {
return ""
}
}
if closeQuote < 0 || closeQuote <= openQuote {
return ""
}
inner := s[openQuote+1 : closeQuote]
quoted := strconv.Quote(inner)
return s[:openQuote] + quoted + s[closeQuote+1:]
}