Conversation
送信元ドメインを用意できなかったため無効化されていたパスワード再設定メールを、 Resend経由での送信に切り替えて復旧させる。 - api/drivers/mail/ を新設し、net/http でResend API(POST /emails)を叩く mail.Client インターフェースを実装(APIキー未設定時はコンソール出力にフォールバック) - password_reset_tokens.go の SendResetEmail を smtp.SendMail から mail.Client 経由の送信に置き換え - di/wire.go に ProvideMailClient を追加(wire_gen.go は make wire-gen が 現状Go 1.25.5環境で実行できないため手動で追従) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This reverts commit cdd89b4.
reset_password/requestページでメールアドレスを送信すると 再設定メール送信の完了表示が出ることを確認する Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deploying finansu with
|
| Latest commit: |
37d6fa7
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2c1ae011.finansu.pages.dev |
| Branch Preview URL: | https://feat-migrate-reset-email-to.finansu.pages.dev |
| }, 120_000); | ||
|
|
||
| test('パスワードリセット申請後に再設定メール送信の完了表示が出る', async ({ page }) => { | ||
| const email = `e2e-reset-${Date.now()}@example.com`; |
There was a problem hiding this comment.
このテストは移行対象コード(api/drivers/mail/mail.go / SendResetEmail)を一度も通りません。
e2e-reset-${Date.now()}@example.comは seed されていないため、PasswordResetTokenRequestがuserRep.FindByEmail→row.Scanでsql.ErrNoRowsを返し、SendResetEmail到達前に return します。PostPasswordResetRequestはエラー時も HTTP 200 を返し、reset_password/request/index.tsxは レスポンスに関係なくsetIsSubmitted(true)するため、Resend 連携が壊れていても/削除されても本テストは常に緑です。
signup.spec.ts のように事前にユーザーを作成(または e2e_seed.sql に対象メールを追加)し、POST のレスポンス内容まで検証してください。
There was a problem hiding this comment.
seedに delivered@resend.dev(Resendのテスト用アドレス)のユーザーを追加し、そのアドレスで申請するようにしました。POSTのステータスとレスポンス本文も検証しています。 37d6fa7
| // SendMail - Resend APIを叩いてメールを送信する | ||
| // RESEND_API_KEYが未設定の場合は送信を行わず、内容をコンソールに出力する | ||
| func (c client) SendMail(to string, subject string, body string) error { | ||
| if c.apiKey == "" { |
There was a problem hiding this comment.
RESEND_API_KEY を mail.go 内で読み、空なら黙って console 出力…という制御はやめたいです。secret 設定漏れ時に「送信しました」と表示されつつ無言で no-op になり、旧 SMTP 実装(認証エラーを返す)より退行します。
方針(相談の結果):
- この
if c.apiKey == ""ブロックと console フォールバックは削除。 - env(
RESEND_API_KEY/MAIL_FROM_ADDRESS)の読み取り・検証は wire 側(ProvideMailClient)に持たせ、空なら error を返して起動失敗させる(ProvideMailClientを(mail.Client, error)に)。 mail.goはNewMailClient(apiKey, fromAddress string)で値を受け取るだけにし、SendMailから分岐を消す。
併せて新 env を compose.stg.yml / compose.prod.yml に追加し、旧 NUTMEG_MAIL_SENDER / NUTMEG_MAIL_PASSWORD は削除してください(現状どの compose にも新 env が無い)。ローカル/e2e は finansu.env に Resend のテストキーを入れる運用で。
There was a problem hiding this comment.
consoleフォールバックを削除し、envの読み取り・検証を ProvideMailClient に移しました。未設定時は起動エラーになります。 37d6fa7
stg/prodのenvについては wire.go 側のコメントに返信しています。
| } | ||
|
|
||
| // NewMailClient - Resend経由でメールを送信するmail.Clientを生成する | ||
| func NewMailClient() client { |
There was a problem hiding this comment.
公開関数が未公開型 client を返しています(.golangci.yml の revive unexported-return に該当、CI で指摘されます)。
func NewMailClient(apiKey, fromAddress string) Client として interface を返してください(引数化は下のコメント参照)。
There was a problem hiding this comment.
NewMailClient(apiKey, fromAddress string) Client に変更しました。 37d6fa7
| return err | ||
| } | ||
|
|
||
| req, err := http.NewRequest(http.MethodPost, resendEndpoint, bytes.NewReader(reqBody)) |
There was a problem hiding this comment.
SendResetEmailはcontext.Contextを持っていますが、ここでhttp.NewRequestを使っており コンテキストが伝播しません。http.NewRequestWithContextに変え、SendMailの I/F にもctxを追加してください。- 成功(2xx)時に
resp.Bodyを読まずCloseしているため keep-alive 接続が再利用されません。io.Copy(io.Discard, resp.Body)を挟むと良いです。
影響は小(10s タイムアウト・低頻度)ですが、ついで修正推奨。
There was a problem hiding this comment.
http.NewRequestWithContext に変更し、SendMail のI/Fに ctx を追加しました。成功時は io.Copy(io.Discard, resp.Body) で読み捨てています。 37d6fa7
| test.beforeAll(async () => { | ||
| await waitForService(`${apiURL}/`); | ||
| await waitForService(`${process.env.BASE_URL || 'http://view:3000'}/`); | ||
| }, 120_000); |
There was a problem hiding this comment.
Playwright の test.beforeAll はフック関数(+任意の title)しか受け取らず、第2引数 120_000 は無視されます。
そのためフックは playwright.config.ts の timeout: 60_000 で動作し、waitForService 内部の 120s デッドラインより先に 60s で中断され得ます。フック内で test.setTimeout(120_000) を使ってください。
(signup.spec.ts からのコピー由来なので、そちらも合わせて修正推奨)
There was a problem hiding this comment.
test.setTimeout(120_000) に修正しました。signup.spec.ts も同様に修正しています。 37d6fa7
| import { useState } from 'react'; | ||
|
|
||
| import { PrimaryButton } from '@/components/common'; | ||
| import Link from '@/components/common/Link'; |
There was a problem hiding this comment.
直上で { PrimaryButton } を barrel @/components/common から import しており、barrel は Link も re-export しています(reset_password/request/index.tsx は import { Link } from '@components/common')。
import { PrimaryButton, Link } from '@/components/common'; に統一を。(cosmetic)
There was a problem hiding this comment.
import { Link, PrimaryButton } from '@/components/common'; に統一しました。 37d6fa7
| } | ||
|
|
||
| // ProvideMailClient - MailClientのProvider | ||
| func ProvideMailClient() mail.Client { |
There was a problem hiding this comment.
env(RESEND_API_KEY / MAIL_FROM_ADDRESS)の読み取り・検証はここに持たせたいです(mail.go 側では読まない)。
func ProvideMailClient() (mail.Client, error) {
apiKey := os.Getenv("RESEND_API_KEY")
if apiKey == "" {
return nil, errors.New("RESEND_API_KEY is not set")
}
from := os.Getenv("MAIL_FROM_ADDRESS")
if from == "" {
return nil, errors.New("MAIL_FROM_ADDRESS is not set")
}
return mail.NewMailClient(apiKey, from), nil
}ProvideMinioClientと同じ(T, error)パターン。wire.Buildはそのまま、make genでwire_gen.goがmailClient, err := ProvideMailClient()に変わります。- これで key 未設定なら起動時に落ちる(本番でのサイレント no-op を防止)。console フォールバックは廃止。
mail.goはNewMailClient(apiKey, fromAddress string) Clientで受け取るだけに。
There was a problem hiding this comment.
ご提示の形で ProvideMailClient を (mail.Client, error) にし、make gen で wire_gen.go を再生成しました。 37d6fa7
stg/prodのenvについて:どちらも env_file でenvを読み込んでいるので、RESEND_API_KEY はシークレットとしてcomposeには直接書かず、サーバー側のenvファイルに追加する方針にしたいです。問題なければそれで進めます。
|
|
||
| if resp.StatusCode >= 300 { | ||
| respBody, _ := io.ReadAll(resp.Body) | ||
| return fmt.Errorf("resend api error: status=%d body=%s", resp.StatusCode, string(respBody)) |
There was a problem hiding this comment.
抜きだせそうだからbodyじゃなくてそのなかみのmessageをレスポンスかなー
var e struct{ Name, Message string }
_ = json.Unmarshal(respBody, &e)
return fmt.Errorf("resend api error: status=%d name=%s message=%s", resp.StatusCode, e.Name, e.Message)
There was a problem hiding this comment.
Resendのエラーレスポンスから name / message を取り出して返すようにしました。 37d6fa7
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| if resp.StatusCode >= 300 { |
There was a problem hiding this comment.
300以上にしてるのって理由ある?
正常系は基本200だけだけどresendだと204でも正常扱いすると勝手ある感じ?特にないなら
200以外 != 200 みたいな感じにしたい。
There was a problem hiding this comment.
特に理由はなかったです。Resendの送信APIは正常時200なので != http.StatusOK にしました。 37d6fa7
| name + " 様\n\n" + | ||
| "情報局 FinanSu 担当です。\r\n\r\n" + | ||
| func (pr *passwordResetTokenRepository) SendResetEmail(c context.Context, id string, name string, email string, token string) error { | ||
| resetPageUrl := os.Getenv("RESET_PASSWORD_URL") + "/" + id + "/?token=" + token |
There was a problem hiding this comment.
[imo]
ちょっと例だけど、こういうのでパス形成したい感あるな―。
特に+でも問題はないけど個人的に
u, err := url.Parse(os.Getenv("RESET_PASSWORD_URL"))
if err != nil {
return err
}
u = u.JoinPath(id) // パス結合("//" や欠落スラッシュを正規化)
u.RawQuery = url.Values{"token": {token}}.Encode() // クエリを自動でパーセントエンコード
resetPageURL := u.String()
There was a problem hiding this comment.
url.Parse → JoinPath → url.Values で組み立てるように変更しました。 37d6fa7
| "※このメールは送信専用です\n" | ||
|
|
||
| return err | ||
| return pr.mailClient.SendMail(email, "【FinanSu】パスワード再設定メール", body) |
There was a problem hiding this comment.
ここbody作ったなら一応、titleも定義してから渡したいなー
- mail.Clientの環境変数読み取り・検証をProvideMailClientに移し、未設定時は起動失敗させる - RESEND_API_KEY未設定時のconsoleフォールバックを削除 - NewMailClientがClient interfaceを返すように変更 - SendMailにcontextを追加し、NewRequestWithContextを使用 - ステータス判定を!= 200にし、Resendのエラーからname/messageを抽出 - リセットURLをnet/urlで組み立て、件名を変数化 - E2E: seedユーザー(delivered@resend.dev)で申請し、レスポンスまで検証 - E2E: beforeAllのタイムアウトをtest.setTimeoutで指定 - index.tsxのLink importをbarrelに統一 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
対応Issue
不明
概要
パスワードリセットメールの送信手段を Gmail SMTP から Resend API に移行しました。
api/drivers/mail/mail.goを新規追加し、Resend の/emailsエンドポイントを叩くmail.Clientを実装RESEND_API_KEY/MAIL_FROM_ADDRESS環境変数を使用RESEND_API_KEY未設定時は実送信せず、内容をコンソール出力するフォールバック動作にしています(ローカル開発用)password_reset_tokens.goのSendResetEmailからnet/smtpを使った Gmail SMTP 送信処理を削除し、mail.Client経由の送信に置き換えdi/wire.go/wire_gen.go) にProvideMailClientを追加し、mail.Clientを注入できるように変更画面スクリーンショット等
URLスクリーンショット
テスト項目
RESEND_API_KEY未設定時にエラーにならずコンソール出力にフォールバックすることpassword_reset.spec.ts) がPASSすること備考