はじめに
本記事は以下の記事で実装したSignUp機能を持つAPIに自動SignInとSignOutの機能を追加するものになります。
実装する機能
1. サインイン
2. 自動サインイン (セッション情報の取得)
3. サインアウト (セッションの無効化)
コードの変更
前回の記事にて作成したAPIに変更を加えていきます。
セッション管理の拡張
infrastructure/session/session_manager.go
に新しいメソッドを追加します。
-
GetUserID()
: セッションからユーザーIDを取得 -
InvalidateSession()
: セッションを無効化
package session
// 省略
type ISessionManager interface {
CreateSession(w http.ResponseWriter, r *http.Request, userID uint) error
// ↓↓追加部分(2行)
GetUserID(r *http.Request) (uint, bool)
InvalidateSession(w http.ResponseWriter, r *http.Request) error
}
// 省略
// 以下全て追加
func (sm *SessionManager) GetUserID(r *http.Request) (uint, bool) {
session, err := sm.store.Get(r, "session-name")
if err != nil {
return 0, false
}
userID, ok := session.Values["user_id"].(uint)
return userID, ok
}
func (sm *SessionManager) InvalidateSession(w http.ResponseWriter, r *http.Request) error {
session, err := sm.store.Get(r, "session-name")
if err != nil {
return err
}
// セッションを無効化
session.Options.MaxAge = -1
return session.Save(r, w)
}
ハンドラーの追加
interface/handler/user_handler.go
に新しいメソッドを実装:
-
SignIn()
: ログイン処理 -
SignOut()
: ログアウト処理 -
GetCurrentUser()
: 現在のユーザー情報を取得
type (
SignUpRequest = application.CreateUserInput
// ↓↓↓追加部分(1行)
SignInRequest = application.SignInUserInput
)
type IUserHandler interface {
SignUp(ctx *gin.Context)
// ↓↓↓追加部分(3行)
SignIn(ctx *gin.Context)
SignOut(ctx *gin.Context)
GetCurrentUser(ctx *gin.Context)
}
// 省略
// ログイン処理
func (h *UserHandler) SignIn(ctx *gin.Context) {
var request SignInRequest
if err := ctx.ShouldBindJSON(&request); err != nil {
ctx.JSON(400, gin.H{"error": err.Error()})
}
id, err := h.userUsecase.SignIn(ctx, request)
if err != nil {
ctx.JSON(500, gin.H{"error": err.Error()})
}
if err := h.sessionManager.CreateSession(ctx.Writer, ctx.Request, id); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Session creation failed"})
return
}
}
// ログアウト処理
func (h *UserHandler) SignOut(ctx *gin.Context) {
if err := h.sessionManager.InvalidateSession(ctx.Writer, ctx.Request); err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "Logout failed"})
return
}
ctx.JSON(http.StatusOK, gin.H{"message": "sign out successful"})
}
// 現在のユーザー情報を取得
func (h *UserHandler) GetCurrentUser(ctx *gin.Context) {
userID, exists := h.sessionManager.GetUserID(ctx.Request)
if !exists {
ctx.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
user, err := h.userUsecase.FindByID(ctx, userID)
if err != nil {
ctx.JSON(http.StatusInternalServerError, gin.H{"error": "User not found"})
return
}
ctx.JSON(http.StatusOK, gin.H{
"id": user.ID,
"name": user.Username,
"email": user.Email,
})
}
ルーティングの更新
main.go
に新しいエンドポイントを追加
func main() {
// 省略
// ルーティング
router := gin.Default()
router.POST("/api/user/signup", userHandler.SignUp)
// ↓↓↓↓追加部分(3行)
router.POST("/api/user/signin", userHandler.SignIn)
router.POST("/api/user/signout", userHandler.SignOut)
router.GET("/api/user/me", userHandler.GetCurrentUser)
log.Fatal(http.ListenAndServe(":8080", router))
}
その他: DB操作部分
ユースケースの追加
-
SignIn()
: EmailとPasswordを使って認証 -
FindByID()
: IDを使ってuserデータを取り出す
package application
// 省略
// 追加部分(type2つ)
type SignInUserInput struct {
Email string `json:"email"`
Password string `json:"password"`
}
type UserOutput struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
type IUserUsecase interface {
Create(ctx context.Context, input CreateUserInput) (uint, error)
// 追加部分(2行)
SignIn(ctx context.Context, input SignInUserInput) (uint, error)
FindByID(ctx context.Context, id uint) (*UserOutput, error)
}
// 省略
// 以下を追加
func (u *UserUsecase) FindByID(ctx context.Context, id uint) (*UserOutput, error) {
user, err := u.userRepo.FindByID(ctx, id)
if err != nil {
return nil, err
}
var userOutput UserOutput
userOutput.ID = user.ID
userOutput.Username = user.Name
userOutput.Email = user.Email
return &userOutput, nil
}
func (u *UserUsecase) SignIn(ctx context.Context, input SignInUserInput) (uint, error) {
// ユーザー検索
user, err := u.userRepo.FindByEmail(ctx, input.Email)
if err != nil {
return 0, err
}
// パスワード検証
err = bcrypt.CompareHashAndPassword(
[]byte(user.Password),
[]byte(input.Password),
)
if err != nil {
return 0, err
}
return user.ID, nil
}
repository
domain/repository/iuser_repository.go
で追加するDBアクセス部分のinterfaceを定義する
package repository
// 省略
type IUserRepository interface {
Create(ctx context.Context, user *model.User) (uint, error)
// 追加部分(2行)
FindByID(ctx context.Context, id uint) (*model.User, error)
FindByEmail(ctx context.Context, email string) (*model.User, error)
}
dao
infrastructure/dao/user.go
でデータベース操作部分を実装する。
-
FindByID()
: IDを使ってDBからuserデータを取り出す -
FindByEmail()
: Emailを使ってDBからuserデータを取り出す
package dao
// 省略
// 以下を追加
func (r *userRepository) FindByID(ctx context.Context, id uint) (*model.User, error) {
user := &model.User{}
err := r.db.WithContext(ctx).Where("id = ?", id).First(&user).Error
if err != nil {
return nil, err
}
return user, nil
}
func (r *userRepository) FindByEmail(ctx context.Context, email string) (*model.User, error) {
user := &model.User{}
err := r.db.WithContext(ctx).Where("email = ?", email).First(&user).Error
if err != nil {
return nil, err
}
return user, nil
}
動作確認
SignIn
エンドポイント
# POST
http://localhost:8080/api/user/signin
リクエストボディ
{
"email": "test_email",
"password": "test_password"
}
自動SignIn
エンドポイント
# GET
http://localhost:8080/api/user/me
- SignInやSignUpを行うとCookieに
session-name
のキーとバリューが格納されています - Cookieを使用してバックエンドからuserのデータがレスポンスとして返される
フロントでこのエンドポイントを叩くことでuserが存在するか?存在する場合はログイン処理を飛ばすって感じです。
自動SignOut
エンドポイント
# POST
http://localhost:8080/api/user/signout
- SignOutを行うとCookieに保存されていた
session-name
のキーとバリューがなくなります - これで次回以降は、SignInやSignUpを行う必要があります
おわりに
今回の記事では、前回のSignUp機能に続き、サインイン、自動サインイン、サインアウト機能を実装しました。これによってユーザーの認証機能による基本的なBE実装が完了です。
パスワード変更の実装の仕方などを後日書くつもりです。