문의 드립니다. #50
-
사용 중인 프로그래밍 언어 및 버전flutter, firebase functions, supabase SDK 버전flutter 3.29.3 firebase_core: ^3.12.1 supabase_flutter: ^2.8.4 운영 환경개발 환경 (로컬) 질문/문제 설명현재 flutter 에서 인증번호 보내기 함수와 인증번호 확인하기함수를 호출했습니다. 밑에 코드를 추가합니다. 코드 예시// flutter
Future<void> sendOtp() async {
final phoneNumber = controllers['phoneNumber']!.text;
try {
final url = Uri.parse('https://sendotp-ky4mzindqq-uc.a.run.app/sendOtp'); // ✅ 이 부분 확인
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'phone': phoneNumber.replaceAll('-', '')}),
);
final json = jsonDecode(response.body);
if (response.statusCode == 200) {
print('인증번호 전송 성공');
} else if(response.statusCode == 429) {
print('인증 실패: ${json['error']} ${response.statusCode} $json');
} else {
print('인증 실패: ${json['error']} ${response.statusCode} $json');
}
} catch(e, s) {
print('보내기 : $e, $s');
}
}
Future<void> verifyOtp() async {
final phoneNumber = controllers['phoneNumber']!.text;
final smsCode = controllers['phoneVerificationCode']!.text;
final url = Uri.parse('https://verifyotp-ky4mzindqq-uc.a.run.app/verifyOtp');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'phone': phoneNumber.replaceAll('-', ''), 'code': smsCode}),
);
final json = jsonDecode(response.body);
if (response.statusCode == 200) {
print('✅ 인증 성공');
} else {
print('❌ 인증 실패: ${json['error']}');
}
}
// firebase functions 코드
import * as admin from "firebase-admin";
import {onRequest} from "firebase-functions/v2/https";
import * as logger from "firebase-functions/logger";
import {Request, Response} from "express";
import axios from "axios";
import * as dotenv from "dotenv";
import crypto from "crypto";
import {v4 as uuidv4} from "uuid";
dotenv.config();
export const sendOtp = onRequest(
async (req: Request, res: Response): Promise<void> => {
const phone = req.body.phone;
if (!/^[0-9]{10,11}$/.test(phone)) {
res.status(400).json({error: "올바른 전화번호 형식이 아닙니다."});
return;
}
const otp = Math.floor(100000 + Math.random() * 900000).toString();
const apiKey = process.env.COOLSMS_KEY;
const apiSecret = process.env.COOLSMS_SECRET;
const fromPhone = process.env.FROM_PHONE;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const supabaseUrl = process.env.SUPABASE_URL!;
if (!apiKey || !apiSecret || !fromPhone || !supabaseUrl || !supabaseKey) {
res.status(500).json({error: "환경변수 누락: 설정을 확인하세요"});
return;
}
try {
const {data}: { data: { success: boolean; wait_seconds: number }[] } =
await axios.post(
`${supabaseUrl}/rest/v1/rpc/save_otp_request`,
{p_phone: phone, p_otp: otp},
{
headers: {
"apikey": supabaseKey,
"Authorization": `Bearer ${supabaseKey}`,
"Content-Type": "application/json", "Prefer": "return=representation",
},
}
);
const {success, wait_seconds: waitSeconds} = data[0];
if (!success) {
res.status(429).json({
error: "요청 제한",
waitSeconds,
waitMinutes: Math.ceil(waitSeconds / 60),
});
return;
}
const date = new Date().toISOString();
const salt = uuidv4();
const message = `${date}${salt}`;
const signature = crypto
.createHmac("sha256", apiSecret)
.update(message)
.digest("base64");
const headers = {
"Content-Type": "application/json",
"Authorization":
`HMAC-SHA256 apiKey=${apiKey},date=${date},salt=${salt},signature=${signature}`,
};
console.log("📦 CoolSMS message:", message);
console.log("📦 CoolSMS headers:", headers);
console.log("📦 CoolSMS signature:", signature);
const body = {
messages: [
{
to: phone,
from: fromPhone,
text: `[앱] 인증번호는 [${otp}]입니다.`,
},
],
};
await axios.post("https://api.coolsms.co.kr/messages/v4/send",
body, {
headers,
});
res.status(200).json({success: true});
} catch (error: unknown) {
const err = error as { response?: { data?: unknown }; message?: string };
logger.error("OTP 전송 실패:", err.response?.data || err.message);
res.status(500).json({error: "OTP 전송 실패", details: err.message});
}
}
);
export const verifyOtp = onRequest(
async (req: Request, res: Response): Promise<void> => {
const {phone, code} = req.body;
if (!phone || !code) {
res.status(400).json({error: "전화번호와 인증번호를 입력하세요."});
return;
}
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
const supabaseUrl = process.env.SUPABASE_URL!;
try {
const {data}: { data: { otp: string; created_at: string }[] } =
await axios.get(`${supabaseUrl}/rest/v1/otp_requests?phone=eq.${phone}`,
{
headers: {
apikey: supabaseKey,
Authorization: `Bearer ${supabaseKey}`,
},
});
const otpData = data[0];
if (!otpData) {
res.status(404).json({error: "인증 요청 기록이 없습니다."});
return;
}
const isExpired =
Date.now() - new Date(otpData.created_at).getTime() > 180000;
if (isExpired) {
res.status(410).json({error: "인증번호가 만료되었습니다. 다시 요청해주세요."});
return;
}
if (otpData.otp !== code) {
res.status(401).json({error: "인증번호가 일치하지 않습니다."});
return;
}
await axios.delete(
`${supabaseUrl}/rest/v1/otp_requests?phone=eq.${phone}`,
{
headers: {
apikey: supabaseKey,
Authorization: `Bearer ${supabaseKey}`,
},
}
);
res.status(200).json({success: true});
} catch (error: unknown) {
const err = error as { response?: { data?: unknown }; message?: string };
logger.error("OTP 검증 실패:", err.response?.data || err.message);
res.status(500).json({error: "OTP 검증 실패", details: err.message});
}
}
);시도한 해결 방법현재 위의 코드를 계속 조금씩 수정해주었으나, log 를 뜯어본 결과 라고 뜹니다. 기대하는 결과메세지가 성공적으로 전송되어야합니다.. 확인사항
|
Beta Was this translation helpful? Give feedback.
Replies: 2 comments 2 replies
-
|
안녕하세요, 솔라피 기술지원팀입니다. API Key로 발송을 진행하시면 Header의 인증정보를 Bearer가 아니라 HMAC-SHA256으로 진행하셔야 합니다. 관련한 사항은 node.js SDK에 명시된 인증처리 절차 코드를 참고 부탁드립니다. 감사합니다. |
Beta Was this translation helpful? Give feedback.
-
|
현재 해당 코드 참조 해서 수정했습니다. 감사합니다 . 이 상황이 로컬 디버그 모드라서 그런건가요? 릴리즈로 배포하고 나면 메세지가 잘 전송되겠죠? |
Beta Was this translation helpful? Give feedback.
안녕하세요, 솔라피 기술지원팀입니다. API Key로 발송을 진행하시면 Header의 인증정보를 Bearer가 아니라 HMAC-SHA256으로 진행하셔야 합니다. 관련한 사항은 node.js SDK에 명시된 인증처리 절차 코드를 참고 부탁드립니다.
감사합니다.