Skip to content

Commit

Permalink
Merge pull request #14 from taiseidev/feature/create-signup
Browse files Browse the repository at this point in the history
🚀 add: Add user registration API.
  • Loading branch information
taiseidev authored Nov 23, 2024
2 parents b38a51c + 72a223c commit 560fa8c
Show file tree
Hide file tree
Showing 19 changed files with 364 additions and 137 deletions.
2 changes: 2 additions & 0 deletions .cspell/framework-words.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
gorm
joho
godotenv
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ build/

# FVM Version Cache
.fvm/

.env
43 changes: 34 additions & 9 deletions server/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@ package main

import (
"camly-api/cmd/api/routes"
"camly-api/internal/user/handler"
authHandler "camly-api/internal/auth/handler"
authRepository "camly-api/internal/auth/repository"
authService "camly-api/internal/auth/service"
"camly-api/internal/user/repository"
"camly-api/internal/user/service"
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"time"

"github.com/joho/godotenv"
"github.com/labstack/echo/v4"
"gorm.io/driver/mysql"
"gorm.io/gorm"
Expand All @@ -20,16 +24,37 @@ import (
type App struct {
db *gorm.DB
userService *service.UserService
authService *authService.AuthService
cleanup func()
}

func NewDB() *gorm.DB {
if os.Getenv("GO_ENV") == "dev" {
err := godotenv.Load("docker/db/.env")
if err != nil {
log.Fatalln("Failed to load environment variables")
}
}
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_HOST"), os.Getenv("MYSQL_PORT"), os.Getenv("MYSQL_DATABASE"))
// データベースに接続
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Failed to connect to the database")
}
fmt.Println("🚀 DB connected!!")
return db
}

func NewApp(db *gorm.DB) *App {
userRepo := repository.NewUserRepository(db)
userService := service.NewUserService(userRepo)
authRepo := authRepository.NewAuthRepository(db)
authService := authService.NewAuthService(authRepo)

return &App{
db: db,
userService: userService,
authService: authService,
cleanup: func() {
sqlDB, err := db.DB()
if err == nil {
Expand All @@ -46,24 +71,24 @@ func NewApp(db *gorm.DB) *App {
func main() {
e := echo.New()

dsn := "user:password@tcp(camly-db:3306)/camly-db?charset=utf8mb4&parseTime=True&loc=Local"

// データベースに接続
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("failed to connect database: %v", err)
// DB接続
db := NewDB()
if db == nil {
log.Fatal("Database initialization failed")
}

// App インスタンスを作成し、クリーンアップ関数を defer で登録
app := NewApp(db)

// appインスタンスのクリーンナップ
defer app.cleanup()

// ハンドラーの初期化
userHandler := handler.NewUserHandler(app.userService)
authHandler := authHandler.NewAuthHandler(app.authService)

// ルートを登録
// TODO: 必要なミドルウェアの追加やグレースフルシャットダウンの実装を検討する
routes.RegisterRoutes(e, userHandler)
routes.RegisterRoutes(e, authHandler)

// 終了シグナルを受け取るチャネルを作成
quit := make(chan os.Signal, 1)
Expand Down
12 changes: 5 additions & 7 deletions server/cmd/api/routes/routes.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
package routes

import (
"camly-api/internal/user/handler"
authHandler "camly-api/internal/auth/handler"

"github.com/labstack/echo/v4"
)

// 全てのルートを定義
func RegisterRoutes(e *echo.Echo, userHandler *handler.UserHandler) {
func RegisterRoutes(e *echo.Echo, authHandler *authHandler.AuthHandler) {
v1 := e.Group("/api/v1")
// ユーザー関連のルートを定義
userRoutes := v1.Group("/users")
// TODO: Add necessary middleware
// userRoutes.Use(middleware.JWT([]byte("secret")))
userRoutes.POST("", userHandler.CreateUser)
// 認証関連のルートを定義
authRoutes := v1.Group("/auth")
authRoutes.POST("/register", authHandler.SignUp)

}
6 changes: 5 additions & 1 deletion server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ services:
- golang_test_network
depends_on:
- db
environment:
- GO_ENV=dev
env_file:
- .env

db:
container_name: camly-db
Expand All @@ -24,7 +28,7 @@ services:
ports:
- "3306:3306"
env_file:
- ./docker/db/.env_mysql
- ./docker/db/.env
volumes:
- mysql_data:/var/lib/mysql
- ./docker/db/init:/docker-entrypoint-initdb.d
Expand Down
4 changes: 0 additions & 4 deletions server/docker/db/.env_mysql

This file was deleted.

2 changes: 2 additions & 0 deletions server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ require (
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
Expand Down
5 changes: 5 additions & 0 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,15 @@ github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaC
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
Expand Down
19 changes: 19 additions & 0 deletions server/internal/auth/claims.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// auth/claims.go
package auth

import (
"time"

"github.com/golang-jwt/jwt/v5"
)

type Claims struct {
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}

// Duration for access token validity
const AccessTokenExpiration = time.Minute * 7

// Duration for refresh token validity
const RefreshTokenExpiration = time.Hour * 24 * 7
78 changes: 78 additions & 0 deletions server/internal/auth/handler/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package handler

import (
authService "camly-api/internal/auth/service"
"camly-api/internal/user/model"
"camly-api/internal/utils"
"fmt"
"log"
"net/http"

"github.com/labstack/echo/v4"
)

type AuthHandler struct {
authService *authService.AuthService
}

func NewAuthHandler(authService *authService.AuthService) *AuthHandler {
if authService == nil {
panic("authService cannot be nil")
}
return &AuthHandler{
authService: authService,
}
}

func (h *AuthHandler) SignUp(c echo.Context) error {
var req model.User
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
"details": err.Error(),
})
}

ctx := c.Request().Context()

// Validate input
if err := validateUserInput(&req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Validation failed",
"details": err.Error(),
})
}

// Serviceを呼び出し
tokens, err := h.authService.SignUp(ctx, req)
if err != nil {
log.Printf("failed to create user: %v", err)
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "User creation failed",
"details": "An error occurred while processing your request",
})
}

// レスポンスとしてユーザーを返す
return c.JSON(http.StatusOK, tokens)
}

func validateUserInput(user *model.User) error {
if user.Name == "" {
return fmt.Errorf("name is required")
}
if user.Email == "" {
return fmt.Errorf("email is required")
}
if !utils.IsValidEmail(user.Email) {
return fmt.Errorf("invalid email format")
}
if user.Password == "" {
return fmt.Errorf("password is required")
}
if len(user.Password) < 8 {
return fmt.Errorf("password must be at least 8 characters long")
}
// Additional password complexity checks can be added here
return nil
}
45 changes: 45 additions & 0 deletions server/internal/auth/repository/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package repository

import (
"camly-api/internal/user/model"
"context"
"fmt"

"gorm.io/gorm"
)

type AuthRepository struct {
db *gorm.DB
}

// NOTE(onishi): multiple interfaces in the future
type IAuthRepository interface {
SaveUser(ctx context.Context, user *model.User) error
}

func NewAuthRepository(db *gorm.DB) IAuthRepository {
if db == nil {
panic("db cannot be nil")
}
return &AuthRepository{
db: db,
}
}

func (r *AuthRepository) SaveUser(ctx context.Context, user *model.User) error {
if user == nil {
return fmt.Errorf("user cannot be nil")
}

// データの妥当性検証
if err := user.Validate(); err != nil {
return fmt.Errorf("invalid user data: %w", err)
}

// ユーザー情報の挿入
if err := r.db.WithContext(ctx).Create(user).Error; err != nil {
return fmt.Errorf("failed to save user: %w", err)
}

return nil
}
69 changes: 69 additions & 0 deletions server/internal/auth/service/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package service

import (
"camly-api/internal/auth"
authRepository "camly-api/internal/auth/repository"
"camly-api/internal/user/model"
"context"
"time"

"golang.org/x/crypto/bcrypt"
)

type AuthService struct {
authRepo authRepository.IAuthRepository
}

func NewAuthService(authRepo authRepository.IAuthRepository) *AuthService {
return &AuthService{
authRepo: authRepo,
}
}

type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
AccessTokenExpiration string `json:"access_token_expiration"`
RefreshTokenExpiration string `json:"refresh_token_expiration"`
}

func (s *AuthService) SignUp(ctx context.Context, user model.User) (TokenResponse, error) {

// パスワードをハッシュ化
hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 10)

if err != nil {
return TokenResponse{}, err
}

newUser := model.User{Name: user.Name, Email: user.Email, Password: string(hash)}

if err := s.authRepo.SaveUser(ctx, &newUser); err != nil {
return TokenResponse{}, err
}

// アクセストークン生成
accessToken, err := auth.GenerateAccessToken(newUser.ID)
if err != nil {
return TokenResponse{}, err
}

// リフレッシュトークン生成
refreshToken, err := auth.GenerateRefreshToken(newUser.ID)
if err != nil {
return TokenResponse{}, err
}

// 有効期限を取得
accessTokenExpiration := time.Now().Add(auth.AccessTokenExpiration).Format(time.RFC3339)
refreshTokenExpiration := time.Now().Add(auth.RefreshTokenExpiration).Format(time.RFC3339)

tokenRes := TokenResponse{
AccessToken: accessToken,
RefreshToken: refreshToken,
AccessTokenExpiration: accessTokenExpiration,
RefreshTokenExpiration: refreshTokenExpiration,
}

return tokenRes, nil
}
Loading

0 comments on commit 560fa8c

Please sign in to comment.