feat: 購入後マジックリンクメール自動送信 + TTS に purchaseMiddleware 接続
Made-with: Cursor
This commit is contained in:
parent
5f371c3eee
commit
10402464c5
|
|
@ -94,6 +94,69 @@ module.exports = function createStripeHandler(pool) {
|
|||
await sendLicenseEmail(email, licenseKey);
|
||||
}
|
||||
|
||||
// ── 購入完了メール(マジックリンク付き)送信 ──
|
||||
async function sendWelcomeMagicLink(email) {
|
||||
if (!RESEND_API_KEY) {
|
||||
console.warn('[Stripe] RESEND_API_KEY not set — skipping welcome email');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// マジックリンク用トークンを生成(15分有効)
|
||||
const token = crypto.randomBytes(32).toString('hex');
|
||||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
|
||||
await pool.query(
|
||||
`INSERT INTO magic_link_tokens (email, token, expires_at) VALUES ($1, $2, $3)`,
|
||||
[email, token, expiresAt]
|
||||
);
|
||||
|
||||
const loginBase = process.env.MAGIC_LINK_BASE_URL || 'https://posimai.soar-enrich.com';
|
||||
const magicLinkUrl = `${loginBase}/auth/verify?token=${token}&redirect=${encodeURIComponent('https://brief.posimai.soar-enrich.com')}`;
|
||||
|
||||
const emailRes = await fetch('https://api.resend.com/emails', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${RESEND_API_KEY}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from: FROM_EMAIL,
|
||||
to: email,
|
||||
subject: 'Posimai へようこそ — ログインリンク',
|
||||
html: `
|
||||
<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px">
|
||||
<h2 style="font-size:20px;font-weight:600;margin-bottom:8px">ご購入ありがとうございます</h2>
|
||||
<p style="color:#555;line-height:1.6">Posimai パスの購入が完了しました。<br>
|
||||
下のボタンをクリックするとすぐにアプリをご利用いただけます。</p>
|
||||
<div style="margin:24px 0">
|
||||
<a href="${magicLinkUrl}"
|
||||
style="display:inline-block;background:#6EE7B7;color:#0D0D0D;
|
||||
font-weight:600;padding:12px 28px;border-radius:8px;
|
||||
text-decoration:none;font-size:15px">
|
||||
Posimai を開く
|
||||
</a>
|
||||
</div>
|
||||
<p style="font-size:12px;color:#888">このリンクは15分間有効です。<br>
|
||||
期限切れの場合は <a href="${loginBase}/login">${loginBase}/login</a> からログインしてください。</p>
|
||||
<hr style="border:none;border-top:1px solid #eee;margin:24px 0">
|
||||
<p style="font-size:12px;color:#aaa">
|
||||
ご不明な点は <a href="mailto:${APP_SUPPORT_EMAIL}">${APP_SUPPORT_EMAIL}</a> までお問い合わせください。
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!emailRes.ok) {
|
||||
const errBody = await emailRes.text();
|
||||
console.error('[Stripe] Welcome email error:', emailRes.status, errBody);
|
||||
} else {
|
||||
console.log(`[Stripe] Welcome magic link sent to ${email}`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Stripe] sendWelcomeMagicLink error:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Posimai 購読購入処理(既存フロー) ──
|
||||
async function handlePosiPurchase(session) {
|
||||
const email = session.customer_details?.email?.toLowerCase();
|
||||
|
|
@ -121,6 +184,11 @@ module.exports = function createStripeHandler(pool) {
|
|||
[session.id, session.customer, session.subscription, userId]
|
||||
);
|
||||
console.log(`[Stripe] Plan upgraded to premium for ${email}`);
|
||||
|
||||
// 購入完了メールにマジックリンクを同梱(非同期・失敗しても購入処理には影響しない)
|
||||
sendWelcomeMagicLink(email).catch(e =>
|
||||
console.error('[Stripe] Welcome email failed (non-critical):', e.message)
|
||||
);
|
||||
}
|
||||
|
||||
// ── Webhook エントリーポイント ──
|
||||
|
|
|
|||
|
|
@ -2159,8 +2159,8 @@ ${excerpt}
|
|||
return Buffer.from(await synthRes.arrayBuffer());
|
||||
}
|
||||
|
||||
// POST /tts — テキストを音声(WAV)に変換して返す
|
||||
r.post('/tts', authMiddleware, async (req, res) => {
|
||||
// POST /tts — テキストを音声(WAV)に変換して返す(購入済みユーザーのみ)
|
||||
r.post('/tts', authMiddleware, purchaseMiddleware, async (req, res) => {
|
||||
const { text, speaker = 1 } = req.body || {};
|
||||
if (!text || typeof text !== 'string') return res.status(400).json({ error: 'text required' });
|
||||
if (text.length > 600) return res.status(400).json({ error: 'text too long (max 600 chars)' });
|
||||
|
|
@ -2203,7 +2203,7 @@ ${excerpt}
|
|||
});
|
||||
|
||||
// POST /tts/ready — 指定テキストがキャッシュ済みか確認(ポーリング用)
|
||||
r.post('/tts/ready', authMiddleware, (req, res) => {
|
||||
r.post('/tts/ready', authMiddleware, purchaseMiddleware, (req, res) => {
|
||||
const { texts, speaker = 1 } = req.body || {};
|
||||
if (!texts || !Array.isArray(texts)) return res.json({ cached: 0, total: 0, ready: false });
|
||||
const total = texts.length;
|
||||
|
|
@ -2213,7 +2213,7 @@ ${excerpt}
|
|||
|
||||
// POST /tts/warmup — バックグラウンドで事前合成してキャッシュを温める
|
||||
// ブラウザが Feed 読み込み直後に呼び出す。即座に 202 を返し、VOICEVOX をバックグラウンドで実行。
|
||||
r.post('/tts/warmup', authMiddleware, async (req, res) => {
|
||||
r.post('/tts/warmup', authMiddleware, purchaseMiddleware, async (req, res) => {
|
||||
const { texts, speaker = 1 } = req.body || {};
|
||||
if (!texts || !Array.isArray(texts) || texts.length === 0) {
|
||||
return res.status(400).json({ error: 'texts[] required' });
|
||||
|
|
|
|||
Loading…
Reference in New Issue