forked from baron/baron-sso
- Added support for fixed UUIDs during bulk registration (Search-first + ExternalID mapping) - Implemented idempotency and visibility restoration for soft-deleted users - Enhanced bulk upload UI to show 'New/Updated/Unchanged' status and modified fields - Added logic to reclaim identifiers (login_id) from colliding records - Added frontend E2E and backend unit tests for UUID integrity and conflict handling - Fixed i18n, formatting, and mock tests to satisfy code-check - Applied 'go fix' for 'omitzero' tags and general Go standards
135 lines
3.6 KiB
Go
135 lines
3.6 KiB
Go
package service
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const naverSMSMaxBytes = 90
|
|
|
|
type SmsServiceImpl struct {
|
|
accessKey string
|
|
secretKey string
|
|
serviceID string
|
|
senderPhone string
|
|
}
|
|
|
|
func NewSmsService() domain.SmsService {
|
|
// Sanitize sender phone number right after reading from env
|
|
rawSenderPhone := os.Getenv("NAVER_SENDER_PHONE_NUMBER")
|
|
sanitizedSenderPhone := strings.ReplaceAll(rawSenderPhone, "-", "")
|
|
slog.Info("[서비스 초기화] 발신자 번호 처리", "원본", rawSenderPhone, "정제후", sanitizedSenderPhone)
|
|
|
|
return &SmsServiceImpl{
|
|
accessKey: os.Getenv("NAVER_CLOUD_ACCESS_KEY"),
|
|
secretKey: os.Getenv("NAVER_CLOUD_SECRET_KEY"),
|
|
serviceID: os.Getenv("NAVER_CLOUD_SERVICE_ID"),
|
|
senderPhone: sanitizedSenderPhone,
|
|
}
|
|
}
|
|
|
|
func (s *SmsServiceImpl) SendSms(to, content string) error {
|
|
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
|
|
apiURL := fmt.Sprintf("https://sens.apigw.ntruss.com/sms/v2/services/%s/messages", s.serviceID)
|
|
slog.Info("[SmsService] Requesting SENS API URL", "url", apiURL)
|
|
|
|
// Naver SENS API requires phone number without '+'
|
|
sanitizedTo := strings.Replace(to, "+", "", 1)
|
|
|
|
reqBody := buildNaverSmsRequest(s.senderPhone, sanitizedTo, content)
|
|
if reqBody.Type == "LMS" {
|
|
slog.Info("[SmsService] Upgrading message type to LMS due to content length",
|
|
"bytes", len([]byte(content)),
|
|
)
|
|
}
|
|
|
|
jsonBody, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("error marshalling request body: %w", err)
|
|
}
|
|
|
|
signature, err := s.makeSignature("POST", fmt.Sprintf("/sms/v2/services/%s/messages", s.serviceID), timestamp)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating signature: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBody))
|
|
if err != nil {
|
|
return fmt.Errorf("error creating request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("x-ncp-apigw-timestamp", timestamp)
|
|
req.Header.Set("x-ncp-iam-access-key", s.accessKey)
|
|
req.Header.Set("x-ncp-apigw-signature-v2", signature)
|
|
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("error sending request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading response body: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode >= 300 {
|
|
slog.Error("[SmsService] error response from naver cloud sms api", "body", string(respBody))
|
|
return fmt.Errorf("error sending sms: status code %d", resp.StatusCode)
|
|
}
|
|
|
|
slog.Info("[SmsService] sms sent successfully", "body", string(respBody))
|
|
return nil
|
|
}
|
|
|
|
func buildNaverSmsRequest(senderPhone, sanitizedTo, content string) domain.NaverSmsRequest {
|
|
requestType := "SMS"
|
|
subject := ""
|
|
if len([]byte(content)) > naverSMSMaxBytes {
|
|
requestType = "LMS"
|
|
subject = "[Baron 로그인]"
|
|
}
|
|
|
|
return domain.NaverSmsRequest{
|
|
Type: requestType,
|
|
ContentType: "COMM",
|
|
CountryCode: "82",
|
|
From: senderPhone,
|
|
Subject: subject,
|
|
Content: content,
|
|
Messages: []domain.SmsMessage{
|
|
{
|
|
To: sanitizedTo,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (s *SmsServiceImpl) makeSignature(method, url, timestamp string) (string, error) {
|
|
space := " "
|
|
newLine := "\n"
|
|
message := method + space + url + newLine + timestamp + newLine + s.accessKey
|
|
|
|
h := hmac.New(sha256.New, []byte(s.secretKey))
|
|
_, err := h.Write([]byte(message))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
|
}
|