diff --git a/routes/stripe.js b/routes/stripe.js
index 9a634c88..132dbbca 100644
--- a/routes/stripe.js
+++ b/routes/stripe.js
@@ -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: `
+
+ `,
+ }),
+ });
+
+ 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 エントリーポイント ──
diff --git a/server.js b/server.js
index 4be2d6f0..d01c14a0 100644
--- a/server.js
+++ b/server.js
@@ -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' });