From b70960eb51ee9b97c760fcaab1f3a21599ac6056 Mon Sep 17 00:00:00 2001 From: KazumaSun Date: Mon, 4 Mar 2024 04:26:32 +0000 Subject: [PATCH] =?UTF-8?q?[feat]=E3=83=91=E3=82=B9=E3=83=AF=E3=83=BC?= =?UTF-8?q?=E3=83=89=E5=A4=89=E6=9B=B4=E3=81=AE=E3=83=A1=E3=83=BC=E3=83=AB?= =?UTF-8?q?=E9=80=81=E4=BF=A1=E6=A9=9F=E8=83=BD=E3=81=AEAPI=E3=81=AE?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/docs/docs.go | 2 +- .../controller/mail_auth_controller.go | 7 +- .../repository/mail_auth_repository.go | 16 +++- .../sesstion_reset_password_repository.go | 66 ++++++++++++++++ api/internals/di/di.go | 3 +- api/internals/domain/session.go | 9 +++ api/internals/usecase/mail_auth_usecase.go | 79 ++++++++++++------- api/router/router.go | 2 +- docker-compose.prod.yml | 2 + docker-compose.yml | 2 + mysql/db/session_reset_password.sql | 11 +++ 11 files changed, 162 insertions(+), 37 deletions(-) create mode 100644 api/externals/repository/sesstion_reset_password_repository.go create mode 100644 mysql/db/session_reset_password.sql diff --git a/api/docs/docs.go b/api/docs/docs.go index 53709f15b..edf41da56 100644 --- a/api/docs/docs.go +++ b/api/docs/docs.go @@ -1991,7 +1991,7 @@ const docTemplate = `{ }, }, }, - "/mail_auth/reset_password":{ + "/mail_auth/send_reset_password":{ "post": { tags: ["email"], "description": "パスワードリセットのメール送信", diff --git a/api/externals/controller/mail_auth_controller.go b/api/externals/controller/mail_auth_controller.go index 8c20a8511..a7333407b 100644 --- a/api/externals/controller/mail_auth_controller.go +++ b/api/externals/controller/mail_auth_controller.go @@ -15,7 +15,7 @@ type MailAuthController interface { SignIn(echo.Context) error SignOut(echo.Context) error IsSignIn(echo.Context) error - ResetPassword(echo.Context) error + SendResetPassword(echo.Context) error } func NewMailAuthController(u usecase.MailAuthUseCase) MailAuthController { @@ -72,11 +72,12 @@ func (auth *mailAuthController) IsSignIn(c echo.Context) error { } // reset password -func (auth *mailAuthController) ResetPassword(c echo.Context) error { +func (auth *mailAuthController) SendResetPassword(c echo.Context) error { email := c.QueryParam("email") - err := auth.u.ResetPassword(c.Request().Context(), email) + token, err := auth.u.SendResetPassword(c.Request().Context(), email) if err != nil { return err } + c.JSON(http.StatusOK, token) return nil } \ No newline at end of file diff --git a/api/externals/repository/mail_auth_repository.go b/api/externals/repository/mail_auth_repository.go index 035485818..8866c8810 100644 --- a/api/externals/repository/mail_auth_repository.go +++ b/api/externals/repository/mail_auth_repository.go @@ -18,7 +18,7 @@ type MailAuthRepository interface { CreateMailAuth(context.Context, string, string, string) (int64, error) FindMailAuthByEmail(context.Context, string) *sql.Row FindMailAuthByID(context.Context, string) *sql.Row - ResetPassword(context.Context, []string) error + SendResetPassword(context.Context, []string) error } func NewMailAuthRepository(client db.Client) MailAuthRepository { @@ -49,7 +49,7 @@ func (r *mailAuthRepository) FindMailAuthByID(c context.Context, id string) *sql } // reset password -func (r *mailAuthRepository) ResetPassword(c context.Context, email []string) error { +func (r *mailAuthRepository) SendResetPassword(c context.Context, email []string) error { err := godotenv.Load("env/dev.env") if err != nil { fmt.Println(err) @@ -57,7 +57,15 @@ func (r *mailAuthRepository) ResetPassword(c context.Context, email []string) er mailSender := os.Getenv("NUTMEG_MAIL_SENDER") mailPassword := os.Getenv("NUTMEG_MAIL_PASSWORD") - message := []byte("test") + resetPageUrl := os.Getenv("RESET_PASSWORD_URL") + + message := []byte("From: 情報局 <" + mailSender + ">\r\n" + + "Subject: FinanSu パスワードリセットの確認メール\r\n\r\n" + + "お世話になっております。\r\n情報局 FinanSu 担当です。\r\n\r\n" + + "FinanSuに登録している本メールアドレスのパスワードをリセットするためには、下記のURLから手続きを行ってください。\r\n" + + "なお、パスワードのリセットの有効期限は本メールが送信されてから10分間とさせていただきます。\r\n" + + "今後ともよろしくお願いいたします。\r\n\r\n" + + "FinanSu: " + resetPageUrl) smtpHost := "smtp.gmail.com" smtpPort := "587" @@ -71,6 +79,6 @@ func (r *mailAuthRepository) ResetPassword(c context.Context, email []string) er fmt.Println(err) return err } - fmt.Println("Email Sent Successfully!") + fmt.Println("Sent password reset mail for " + email[0]) return nil } diff --git a/api/externals/repository/sesstion_reset_password_repository.go b/api/externals/repository/sesstion_reset_password_repository.go new file mode 100644 index 000000000..0ec506906 --- /dev/null +++ b/api/externals/repository/sesstion_reset_password_repository.go @@ -0,0 +1,66 @@ +package repository + +import ( + "context" + "database/sql" + "github.com/NUTFes/FinanSu/api/drivers/db" + "fmt" +) + + +type sessionResetPasswordRepository struct { + client db.Client +} + +type SessionResetPasswordRepository interface { + Create(context.Context, string, string, string) error + Destroy(context.Context, string) error + FindSessionByAccessToken(context.Context, string) *sql.Row + DestroyByUserID(context.Context, string) error +} + +func NewSessionResetPasswordRepository(client db.Client) SessionResetPasswordRepository { + return &sessionResetPasswordRepository{client} +} + +// 作成 +func (r *sessionResetPasswordRepository) Create(c context.Context, authID string, userID string, accessToken string) error { + query := "insert into session_reset_password (auth_id, user_id, access_token) values (" + authID + ", " + userID + ", '" + accessToken + "')" + _, err := r.client.DB().ExecContext(c, query) + if err != nil { + return err + } + fmt.Printf("\x1b[36m%s\n", query) + return nil +} + +// 削除 +func (r *sessionResetPasswordRepository) Destroy(c context.Context, accessToken string) error { + // access tokenで該当のsessionを削除 + query := "delete from session_reset_password where access_token = '" + accessToken + "'" + _, err := r.client.DB().ExecContext(c, query) + if err != nil { + return err + } + fmt.Printf("\x1b[36m%s\n", query) + return nil +} + +// アクセストークンからセッションを取得 +func (r *sessionResetPasswordRepository) FindSessionByAccessToken(c context.Context, accessToken string) *sql.Row { + query := "select * from session_reset_password where access_token = '" + accessToken + "'" + row := r.client.DB().QueryRowContext(c, query) + fmt.Printf("\x1b[36m%s\n", query) + return row +} + +// user_idからsessionを削除する +func (r *sessionResetPasswordRepository) DestroyByUserID(c context.Context, userID string) error { + query := "delete from session_reset_password where user_id = " + userID + _, err := r.client.DB().ExecContext(c, query) + if err != nil { + return err + } + fmt.Printf("\x1b[36m%s\n", query) + return nil +} diff --git a/api/internals/di/di.go b/api/internals/di/di.go index 17ede41ad..4bc64900e 100644 --- a/api/internals/di/di.go +++ b/api/internals/di/di.go @@ -36,6 +36,7 @@ func InitializeServer() db.Client { purchaseOrderRepository := repository.NewPurchaseOrderRepository(client, crud) purchaseReportRepository := repository.NewPurchaseReportRepository(client, crud) sessionRepository := repository.NewSessionRepository(client) + sessionResetPasswordRepository := repository.NewSessionResetPasswordRepository(client) sourceRepository := repository.NewSourceRepository(client, crud) sponsorRepository := repository.NewSponsorRepository(client, crud) sponsorStyleRepository := repository.NewSponsorStyleRepository(client, crud) @@ -52,7 +53,7 @@ func InitializeServer() db.Client { departmentUseCase := usecase.NewDepartmentUseCase(departmentRepository) expenseUseCase := usecase.NewExpenseUseCase(expenseRepository) fundInformationUseCase := usecase.NewFundInformationUseCase(fundInformationRepository) - mailAuthUseCase := usecase.NewMailAuthUseCase(mailAuthRepository, sessionRepository) + mailAuthUseCase := usecase.NewMailAuthUseCase(mailAuthRepository, sessionRepository, sessionResetPasswordRepository) purchaseItemUseCase := usecase.NewPurchaseItemUseCase(purchaseItemRepository) purchaseOrderUseCase := usecase.NewPurchaseOrderUseCase(purchaseOrderRepository) purchaseReportUseCase := usecase.NewPurchaseReportUseCase(purchaseReportRepository) diff --git a/api/internals/domain/session.go b/api/internals/domain/session.go index 25f6b23ca..520ce7ec5 100644 --- a/api/internals/domain/session.go +++ b/api/internals/domain/session.go @@ -12,3 +12,12 @@ type Session struct { CreatedAt time.Time UpdatedAt time.Time } + +type SessionResetPassword struct { + ID ID + AuthID int + UserID int + AccessToken string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/api/internals/usecase/mail_auth_usecase.go b/api/internals/usecase/mail_auth_usecase.go index 6c2b7c0de..904f945c6 100644 --- a/api/internals/usecase/mail_auth_usecase.go +++ b/api/internals/usecase/mail_auth_usecase.go @@ -14,6 +14,7 @@ import ( type mailAuthUseCase struct { mailAuthRep rep.MailAuthRepository sessionRep rep.SessionRepository + sessionResetPasswordRep rep.SessionResetPasswordRepository } type MailAuthUseCase interface { @@ -21,11 +22,11 @@ type MailAuthUseCase interface { SignIn(context.Context, string, string) (domain.Token, error) SignOut(context.Context, string) error IsSignIn(context.Context, string) (domain.IsSignIn, error) - ResetPassword(context.Context, string) error + SendResetPassword(context.Context, string) (domain.Token, error) } -func NewMailAuthUseCase(mailAuthRep rep.MailAuthRepository, sessionRep rep.SessionRepository) MailAuthUseCase { - return &mailAuthUseCase{mailAuthRep: mailAuthRep, sessionRep: sessionRep} +func NewMailAuthUseCase(mailAuthRep rep.MailAuthRepository, sessionRep rep.SessionRepository, sessionResetPasswordRep rep.SessionResetPasswordRepository) MailAuthUseCase { + return &mailAuthUseCase{mailAuthRep: mailAuthRep, sessionRep: sessionRep, sessionResetPasswordRep: sessionResetPasswordRep} } func (u *mailAuthUseCase) SignUp(c context.Context, email string, password string, userID string) (domain.Token, error) { @@ -86,25 +87,6 @@ func (u *mailAuthUseCase) SignOut(c context.Context, accessToken string) error { return nil } -// アクセストークンを生成 -func _makeRandomStr(digit uint32) (string, error) { - const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - - // 乱数を生成 - b := make([]byte, digit) - if _, err := rand.Read(b); err != nil { - return "", errors.New("unexpected error...") - } - - // letters からランダムに取り出して文字列を生成 - var result string - for _, v := range b { - // index が letters の長さに収まるように調整 - result += string(letters[int(v)%len(letters)]) - } - return result, nil -} - func (u *mailAuthUseCase) IsSignIn(c context.Context, accessToken string) (domain.IsSignIn, error) { var session = domain.Session{} var isSignIn domain.IsSignIn @@ -126,12 +108,55 @@ func (u *mailAuthUseCase) IsSignIn(c context.Context, accessToken string) (domai } // reset password -func (u *mailAuthUseCase) ResetPassword(c context.Context, email string) error { - receiverEmail := []string{email} - err := u.mailAuthRep.ResetPassword(c, receiverEmail) +func (u *mailAuthUseCase) SendResetPassword(c context.Context, email string) (domain.Token, error) { + var mailAuth = domain.MailAuth{} + var token domain.Token + + // メールアドレスの存在確認 + row := u.mailAuthRep.FindMailAuthByEmail(c, email) + err := row.Scan( + &mailAuth.ID, + &mailAuth.Email, + &mailAuth.Password, + &mailAuth.UserID, + &mailAuth.CreatedAt, + &mailAuth.UpdatedAt, + ) + u.sessionResetPasswordRep.DestroyByUserID(c, strconv.Itoa(int(mailAuth.UserID))) + // トークン発行 + accessToken, err := _makeRandomStr(10) + // リセットセッション開始 + err = u.sessionResetPasswordRep.Create(c, strconv.FormatInt(int64(mailAuth.ID), 10), strconv.Itoa(int(mailAuth.UserID)), accessToken) if err != nil { - return err + return token, err } + token.AccessToken = accessToken - return err + // メール送信 + receiverEmail := []string{mailAuth.Email} + err = u.mailAuthRep.SendResetPassword(c, receiverEmail) + if err != nil { + return token, err + } + + return token, nil } + +// アクセストークンを生成 +func _makeRandomStr(digit uint32) (string, error) { + const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + // 乱数を生成 + b := make([]byte, digit) + if _, err := rand.Read(b); err != nil { + return "", errors.New("unexpected error...") + } + + // letters からランダムに取り出して文字列を生成 + var result string + for _, v := range b { + // index が letters の長さに収まるように調整 + result += string(letters[int(v)%len(letters)]) + } + return result, nil +} \ No newline at end of file diff --git a/api/router/router.go b/api/router/router.go index 09528b231..89e300fde 100644 --- a/api/router/router.go +++ b/api/router/router.go @@ -141,7 +141,7 @@ func (r router) ProvideRouter(e *echo.Echo) { e.POST("/mail_auth/signin", r.mailAuthController.SignIn) e.DELETE("/mail_auth/signout", r.mailAuthController.SignOut) e.GET("/mail_auth/is_signin", r.mailAuthController.IsSignIn) - e.POST("/mail_auth/reset_password", r.mailAuthController.ResetPassword) + e.POST("/mail_auth/send_reset_password", r.mailAuthController.SendResetPassword) // purchaseitemsのRoute e.GET("/purchaseitems", r.purchaseItemController.IndexPurchaseItem) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 31e3a0186..836ec49cf 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -25,3 +25,5 @@ services: command: "go run main.go" env_file: ["./finansu.env"] ports: ["1323:1323"] + environment: + RESET_PASSWORD_URL: 'https://finansu.nutfes.net/reset_password' diff --git a/docker-compose.yml b/docker-compose.yml index a74a94277..b91c485cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,6 +34,8 @@ services: container_name: "nutfes-finansu-api" volumes: - ./api:/app + environment: + RESET_PASSWORD_URL: 'https://localhost:3000/reset_password' #シェルスクリプトを実行するコマンド command: "./start.sh" ports: diff --git a/mysql/db/session_reset_password.sql b/mysql/db/session_reset_password.sql new file mode 100644 index 000000000..ccb30362f --- /dev/null +++ b/mysql/db/session_reset_password.sql @@ -0,0 +1,11 @@ +use finansu_db; + +CREATE TABLE session_reset_password ( + id int(10) unsigned not null unique auto_increment, + auth_id int(10) not null, + user_id int(10) not null, + access_token varchar(255) not null, + created_at datetime not null default current_timestamp, + updated_at datetime not null default current_timestamp on update current_timestamp, + PRIMARY KEY (auth_id) +); \ No newline at end of file