forked from baron/baron-sso
SMS 인증을 위한 Naver SENS 연동 및 API 구현
This commit is contained in:
@@ -74,6 +74,7 @@ func main() {
|
|||||||
auth.Post("/enchanted-link/init", authHandler.InitEnchantedLink)
|
auth.Post("/enchanted-link/init", authHandler.InitEnchantedLink)
|
||||||
auth.Post("/enchanted-link/poll", authHandler.PollEnchantedLink)
|
auth.Post("/enchanted-link/poll", authHandler.PollEnchantedLink)
|
||||||
auth.Post("/magic-link/verify", authHandler.VerifyMagicLink)
|
auth.Post("/magic-link/verify", authHandler.VerifyMagicLink)
|
||||||
|
auth.Post("/sms", authHandler.SendSms)
|
||||||
|
|
||||||
// Client Logging Route (For Debugging)
|
// Client Logging Route (For Debugging)
|
||||||
api.Post("/client-log", func(c *fiber.Ctx) error {
|
api.Post("/client-log", func(c *fiber.Ctx) error {
|
||||||
|
|||||||
35
backend/internal/domain/sms_models.go
Normal file
35
backend/internal/domain/sms_models.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
// SmsService defines the interface for sending SMS messages.
|
||||||
|
type SmsService interface {
|
||||||
|
SendSms(to, content string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NaverSmsRequest represents the request body for the Naver Cloud SMS API.
|
||||||
|
type NaverSmsRequest struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
ContentType string `json:"contentType"`
|
||||||
|
CountryCode string `json:"countryCode"`
|
||||||
|
From string `json:"from"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
Messages []SmsMessage `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmsMessage represents a single message to be sent.
|
||||||
|
type SmsMessage struct {
|
||||||
|
To string `json:"to"`
|
||||||
|
Content string `json:"content,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NaverSmsResponse represents the response from the Naver Cloud SMS API.
|
||||||
|
type NaverSmsResponse struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
RequestTime string `json:"requestTime"`
|
||||||
|
StatusCode string `json:"statusCode"`
|
||||||
|
StatusName string `json:"statusName"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SmsRequest represents the request body for sending an SMS.
|
||||||
|
type SmsRequest struct {
|
||||||
|
PhoneNumber string `json:"phoneNumber"`
|
||||||
|
}
|
||||||
@@ -2,18 +2,24 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"baron-sso-backend/internal/domain"
|
"baron-sso-backend/internal/domain"
|
||||||
|
"baron-sso-backend/internal/service"
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthHandler struct {
|
type AuthHandler struct {
|
||||||
ProjectID string
|
ProjectID string
|
||||||
|
SmsService domain.SmsService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewAuthHandler() *AuthHandler {
|
func NewAuthHandler() *AuthHandler {
|
||||||
@@ -22,7 +28,35 @@ func NewAuthHandler() *AuthHandler {
|
|||||||
// Fallback for dev if not set
|
// Fallback for dev if not set
|
||||||
pid = "P37DsGepBT6uDWb5TYYpb5RxUPuq"
|
pid = "P37DsGepBT6uDWb5TYYpb5RxUPuq"
|
||||||
}
|
}
|
||||||
return &AuthHandler{ProjectID: pid}
|
return &AuthHandler{
|
||||||
|
ProjectID: pid,
|
||||||
|
SmsService: service.NewSmsService(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSms sends a verification code via SMS.
|
||||||
|
func (h *AuthHandler) SendSms(c *fiber.Ctx) error {
|
||||||
|
var req domain.SmsRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize phone number: remove dashes
|
||||||
|
sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "")
|
||||||
|
|
||||||
|
// Generate a 6-digit verification code
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||||
|
content := fmt.Sprintf("[Baron SSO] Your verification code is %s", code)
|
||||||
|
|
||||||
|
if err := h.SmsService.SendSms(sanitizedPhone, content); err != nil {
|
||||||
|
log.Printf("Error sending SMS: %v", err)
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Failed to send SMS"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Store the verification code for later verification
|
||||||
|
|
||||||
|
return c.JSON(fiber.Map{"message": "SMS sent successfully"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getBaseURL extracts the region code from Project ID if present (e.g., P37... -> api.37ds.descope.com)
|
// getBaseURL extracts the region code from Project ID if present (e.g., P37... -> api.37ds.descope.com)
|
||||||
|
|||||||
107
backend/internal/service/sms_service.go
Normal file
107
backend/internal/service/sms_service.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SmsServiceImpl struct {
|
||||||
|
accessKey string
|
||||||
|
secretKey string
|
||||||
|
serviceID string
|
||||||
|
senderPhone string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSmsService() domain.SmsService {
|
||||||
|
return &SmsServiceImpl{
|
||||||
|
accessKey: os.Getenv("NAVER_CLOUD_ACCESS_KEY"),
|
||||||
|
secretKey: os.Getenv("NAVER_CLOUD_SECRET_KEY"),
|
||||||
|
serviceID: os.Getenv("NAVER_CLOUD_SERVICE_ID"),
|
||||||
|
senderPhone: os.Getenv("NAVER_SENDER_PHONE_NUMBER"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
log.Printf("Requesting SENS API URL: %s", apiURL)
|
||||||
|
|
||||||
|
reqBody := domain.NaverSmsRequest{
|
||||||
|
Type: "SMS",
|
||||||
|
ContentType: "COMM",
|
||||||
|
CountryCode: "82",
|
||||||
|
From: s.senderPhone,
|
||||||
|
Content: content,
|
||||||
|
Messages: []domain.SmsMessage{
|
||||||
|
{
|
||||||
|
To: to,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error reading response body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
log.Printf("error response from naver cloud sms api: %s", string(respBody))
|
||||||
|
return fmt.Errorf("error sending sms: status code %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("sms sent successfully: %s", string(respBody))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user