149 lines
4.4 KiB
PHP
149 lines
4.4 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Ope;
|
||
use Illuminate\Support\Facades\Hash;
|
||
use Illuminate\Support\Str;
|
||
|
||
/**
|
||
* メール OTP 認証サービス
|
||
*
|
||
* 6桁の OTP コード生成、送信、検証、および24時間免OTPの判定を担当します
|
||
*/
|
||
class EmailOtpService
|
||
{
|
||
// OTP コード長(6桁)
|
||
const OTP_CODE_LENGTH = 6;
|
||
|
||
// OTP 有効期限(分)
|
||
const OTP_VALIDITY_MINUTES = 10;
|
||
|
||
// OTP 重発制限時間(秒)
|
||
const OTP_RESEND_DELAY_SECONDS = 60;
|
||
|
||
// 24時間免OTP期限(時間)
|
||
const OTP_FREE_PERIOD_HOURS = 24;
|
||
|
||
/**
|
||
* OTP コードを生成・保存して送信
|
||
*
|
||
* 6桁の数字コードを生成し、hash化して保存します
|
||
* 同時に有効期限と最後の送信時刻を更新します
|
||
*
|
||
* @param Ope $ope オペレータモデル
|
||
* @return string 明文のOTPコード(メール送信用)
|
||
*/
|
||
public function issue(Ope $ope): string
|
||
{
|
||
// 6桁の数字コードを生成
|
||
$plainCode = str_pad(random_int(0, 999999), self::OTP_CODE_LENGTH, '0', STR_PAD_LEFT);
|
||
|
||
// hash化して保存
|
||
$ope->email_otp_code_hash = Hash::make($plainCode);
|
||
$ope->email_otp_expires_at = now()->addMinutes(self::OTP_VALIDITY_MINUTES);
|
||
$ope->email_otp_last_sent_at = now();
|
||
$ope->save();
|
||
|
||
return $plainCode;
|
||
}
|
||
|
||
/**
|
||
* OTP コードを検証
|
||
*
|
||
* 入力されたコードが正しく、且つ有効期限内かを確認します
|
||
*
|
||
* @param Ope $ope オペレータモデル
|
||
* @param string $inputCode ユーザーが入力したコード
|
||
* @return bool 検証結果
|
||
*/
|
||
public function verify(Ope $ope, string $inputCode): bool
|
||
{
|
||
// 有効期限チェック
|
||
if (!$ope->email_otp_expires_at || $ope->email_otp_expires_at < now()) {
|
||
return false;
|
||
}
|
||
|
||
// hash化されたコードと比較
|
||
if (!Hash::check($inputCode, $ope->email_otp_code_hash)) {
|
||
return false;
|
||
}
|
||
|
||
// 検証成功:検証完了時刻を記録し、OTPコードをクリア
|
||
$ope->email_otp_verified_at = now();
|
||
$ope->email_otp_code_hash = null;
|
||
$ope->email_otp_expires_at = null;
|
||
$ope->save();
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* OTP が最近検証されたかを確認(24時間以内)
|
||
*
|
||
* @param Ope $ope オペレータモデル
|
||
* @return bool true: 24時間以内に検証済み、false: 未検証または24時間以上経過
|
||
*/
|
||
public function isOtpRecent(Ope $ope): bool
|
||
{
|
||
if (!$ope->email_otp_verified_at) {
|
||
return false;
|
||
}
|
||
|
||
return $ope->email_otp_verified_at > now()->subHours(self::OTP_FREE_PERIOD_HOURS);
|
||
}
|
||
|
||
/**
|
||
* 重発可能かを確認
|
||
*
|
||
* 前回送信から60秒以上経過しているかを確認します
|
||
*
|
||
* @param Ope $ope オペレータモデル
|
||
* @return bool true: 重発可能、false: 待機中
|
||
*/
|
||
public function canResend(Ope $ope): bool
|
||
{
|
||
if (!$ope->email_otp_last_sent_at) {
|
||
return true;
|
||
}
|
||
|
||
return $ope->email_otp_last_sent_at <= now()->subSeconds(self::OTP_RESEND_DELAY_SECONDS);
|
||
}
|
||
|
||
/**
|
||
* 次の重発までの待機時間を取得(秒)
|
||
*
|
||
* @param Ope $ope オペレータモデル
|
||
* @return int 待機時間(秒)、0以下は重発可能
|
||
*/
|
||
public function getResendWaitSeconds(Ope $ope): int
|
||
{
|
||
if (!$ope->email_otp_last_sent_at) {
|
||
return 0;
|
||
}
|
||
|
||
$diffSeconds = now()->diffInSeconds($ope->email_otp_last_sent_at, absolute: false);
|
||
$waitSeconds = self::OTP_RESEND_DELAY_SECONDS - abs($diffSeconds);
|
||
|
||
return max(0, $waitSeconds);
|
||
}
|
||
|
||
/**
|
||
* OTP コードのマスク済みメールアドレスを取得
|
||
*
|
||
* abc@example.com -> abc***@example.com のようなマスク表示用
|
||
*
|
||
* @param string $email メールアドレス
|
||
* @return string マスク済みメールアドレス
|
||
*/
|
||
public function maskEmail(string $email): string
|
||
{
|
||
[$name, $domain] = explode('@', $email);
|
||
|
||
// 名前部分の最初の1文字を残して、残りは*でマスク
|
||
$maskedName = substr($name, 0, 1) . str_repeat('*', max(0, strlen($name) - 1));
|
||
|
||
return "{$maskedName}@{$domain}";
|
||
}
|
||
}
|