first commit

This commit is contained in:
2023-10-19 22:47:12 +09:00
commit 7873968e4d
35 changed files with 1710 additions and 0 deletions

View File

@ -0,0 +1,67 @@
package controllers
import (
"net/http"
"studioj/boilerplate_go/internal/models"
"studioj/boilerplate_go/internal/services"
"github.com/gin-gonic/gin"
)
type AuthController interface {
Register(*gin.Context)
Login(*gin.Context)
}
type authController struct {
service services.AuthService
}
func NewAuthController(service services.AuthService) AuthController {
return &authController{
service: service,
}
}
func (controller *authController) Register(c *gin.Context) {
var params models.RegisterRequest
if c.BindJSON(&params) != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
user, err := controller.service.Register(&params)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, err := controller.service.CreateToken(user.ID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"user": user, "token": token.Token, "refresh_token": token.RefreshToken})
}
func (controller *authController) Login(c *gin.Context) {
var params models.LoginRequest
if c.BindJSON(&params) != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
return
}
user, err := controller.service.Login(&params)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
token, err := controller.service.CreateToken(user.ID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"user": user, "token": token.Token, "refresh_token": token.RefreshToken})
}

View File

@ -0,0 +1,59 @@
package controllers
import (
"net/http"
"studioj/boilerplate_go/internal/services"
"github.com/gin-gonic/gin"
)
type TokenController interface {
Find(*gin.Context)
}
type tokenController struct {
service services.TokenService
}
func NewTokenController(service services.TokenService) TokenController {
return &tokenController{
service: service,
}
}
func (controller *tokenController) Find(c *gin.Context) {
id := c.Param("id")
user_id := c.GetString("user_id")
if user_id != id {
c.JSON(http.StatusBadRequest, gin.H{"error": "Wrong user"})
return
}
result, err := controller.service.Find(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}
func (controller *tokenController) List(c *gin.Context) {
id := c.Param("id")
user_id := c.GetString("user_id")
if user_id != id {
c.JSON(http.StatusBadRequest, gin.H{"error": "Wrong user"})
return
}
result, err := controller.service.Find(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}

View File

@ -0,0 +1,46 @@
package controllers
import (
"fmt"
"net/http"
"studioj/boilerplate_go/internal/services"
"github.com/gin-gonic/gin"
)
type UserController interface {
Find(*gin.Context)
}
type userController struct {
service services.UserService
tokenService services.TokenService
}
func NewUserController(service services.UserService, tokenService services.TokenService) UserController {
return &userController{
service: service,
tokenService: tokenService,
}
}
func (controller *userController) Find(c *gin.Context) {
id := c.Param("id")
user_id := c.GetString("sub")
fmt.Println("id", id)
fmt.Println("user_id", user_id)
if user_id != id {
c.JSON(http.StatusBadRequest, gin.H{"error": "Wrong user"})
return
}
result, err := controller.service.FindByID(id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
}

View File

@ -0,0 +1,40 @@
package database
import (
config "studioj/boilerplate_go/configs"
"studioj/boilerplate_go/internal/models"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
type Database struct {
*gorm.DB
}
var DB *gorm.DB
func Init() {
DB = ConnectDB((config.DATABASE_URL))
}
func ConnectDB(url string) *gorm.DB {
gorm_config := gorm.Config{NamingStrategy: schema.NamingStrategy{SingularTable: true}}
db, err := gorm.Open(mysql.Open(url), &gorm_config)
if err != nil {
panic(err)
}
return db
}
func GetDB() *gorm.DB {
return DB
}
func AutoMigrate() {
DB.AutoMigrate(
&models.User{},
&models.Token{},
)
}

View File

@ -0,0 +1,29 @@
package helpers
import (
"math/rand"
"os"
"strconv"
"time"
)
func InLambda() bool {
if lambdaTaskRoot := os.Getenv("LAMBDA_TASK_ROOT"); lambdaTaskRoot != "" {
return true
}
return false
}
func RandomPin() string {
min := 100000
max := 999999
r := rand.New(rand.NewSource(time.Now().UnixNano()))
pin := r.Intn(max-min) + min
return strconv.Itoa(pin)
}
func Datetime(timeString string) (time.Time, error) {
layout := "2006-01-02T15:04:05.000Z"
result, err := time.Parse(layout, timeString)
return result, err
}

View File

@ -0,0 +1,68 @@
package middleware
import (
"errors"
"fmt"
"net/http"
"strings"
config "studioj/boilerplate_go/configs"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
sub, err := UserID(c.Request)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
c.Abort()
return
}
fmt.Println("token", extract(c.Request))
fmt.Println("sub", *sub)
c.Set("token", extract(c.Request))
c.Set("sub", *sub)
c.Next()
}
}
func extract(r *http.Request) string {
authorization := r.Header.Get("Authorization")
strArr := strings.Split(authorization, " ")
if len(strArr) == 2 {
return strArr[1]
}
return ""
}
func verify(r *http.Request) (*jwt.Token, error) {
tokenString := extract(r)
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
}
return []byte(config.SECRET_KEY), nil
})
return jwtToken, err
}
func UserID(r *http.Request) (*string, error) {
jwtToken, err := verify(r)
if err != nil {
return nil, err
}
claims, ok := jwtToken.Claims.(jwt.MapClaims)
if !ok || !jwtToken.Valid {
return nil, errors.New("refresh token is invalid")
}
sub := claims["sub"].(string)
return &sub, nil
}

View File

@ -0,0 +1,44 @@
package middleware
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func StatusInList(status int, statusList []int) bool {
for _, i := range statusList {
for i == status {
return true
}
}
return false
}
func Transaction(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
txHandle := db.Begin()
log.Print("begining database transaction")
defer func() {
if r := recover(); r != nil {
txHandle.Rollback()
}
}()
c.Set("db_trx", txHandle)
c.Next()
if StatusInList(c.Writer.Status(), []int{http.StatusOK, http.StatusCreated}) {
log.Print("committing transactions")
if err := txHandle.Commit().Error; err != nil {
log.Print("transaction commit error: ", err)
} else {
log.Print("rollback transaction due to status code: ", c.Writer.Status())
txHandle.Rollback()
}
}
}
}

11
internal/models/auth.go Normal file
View File

@ -0,0 +1,11 @@
package models
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type RegisterRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}

25
internal/models/token.go Normal file
View File

@ -0,0 +1,25 @@
package models
import "time"
type Token struct {
ID string `json:"id" db:"id" gorm:"column:id;size:255;primary_key;"`
UserID string `json:"user_id" db:"user_id" gorm:"column:user_id;size:255;index;"`
Token string `json:"token" gorm:"column:token;size:255;index;"`
RefreshToken string `json:"refresh_token" gorm:"column:token;size:255;index;"`
Status string `json:"status" gorm:"column:status;size:10;index;"`
ExpireAt time.Time `json:"expire_at" gorm:"column:expire_at;index;"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at;index;"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at;index;"`
}
type TokenResponse struct {
Token string `json:"token"`
TokenBody TokenBody `json:"tokenBody"`
}
type TokenBody struct {
ExpireAt time.Time `json:"tokenExpiredDate"`
TokenIdx int `json:"tokenIdx"`
TokenType int `json:"tokenType"`
}

9
internal/models/user.go Normal file
View File

@ -0,0 +1,9 @@
package models
type User struct {
ID string `json:"id" db:"id" gorm:"column:id;size:255;primary_key;"`
Username string `json:"username" db:"username" gorm:"column:username;size:50;uniqueIndex;"`
Score int32 `json:"score" db:"score" gorm:"column:score;"`
Money int32 `json:"money" db:"money" gorm:"column:money;"`
Password string `json:"-" db:"password" gorm:"column:password;size:255;not null;"`
}

View File

@ -0,0 +1,68 @@
package repositories
import (
config "studioj/boilerplate_go/configs"
"studioj/boilerplate_go/internal/models"
"github.com/golang-jwt/jwt/v5"
"gorm.io/gorm"
)
type tokenRepository struct {
DB *gorm.DB
}
func NewTokenRepository(db *gorm.DB) TokenRepository {
return &tokenRepository{
DB: db,
}
}
type TokenRepository interface {
Generate(string, int64) (string, error)
Find(string) (*models.Token, error)
Create(*models.Token) (*models.Token, error)
Update(*models.Token) (*models.Token, error)
Delete(string) error
}
func (s *tokenRepository) Generate(sub string, expire_at int64) (string, error) {
claims := jwt.MapClaims{}
claims["authorized"] = true
claims["sub"] = sub
claims["exp"] = expire_at
at := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := at.SignedString([]byte(config.SECRET_KEY))
return token, err
}
func (r *tokenRepository) Find(id string) (*models.Token, error) {
var token *models.Token
err := r.DB.Where("id = ?", id).First(&token).Error
return token, err
}
func (r *tokenRepository) Create(token *models.Token) (*models.Token, error) {
err := r.DB.Create(&token).Error
return token, err
}
func (r *tokenRepository) Update(token *models.Token) (*models.Token, error) {
var row *models.Token
if err := r.DB.Where("id=?", token.ID).First(&row).Error; err != nil {
return nil, err
}
err := r.DB.Model(&row).Select("*").Updates(&token).Error
return row, err
}
func (r *tokenRepository) Delete(id string) error {
var token *models.Token
if err := r.DB.Where("id=?", id).First(&token).Error; err != nil {
return err
}
err := r.DB.Delete(&token).Error
return err
}

View File

@ -0,0 +1,68 @@
package repositories
import (
"studioj/boilerplate_go/internal/models"
"gorm.io/gorm"
)
type userRepository struct {
DB *gorm.DB
}
func NewUserRepository(db *gorm.DB) UserRepository {
return &userRepository{
DB: db,
}
}
type UserRepository interface {
List() (*[]models.User, error)
FindByID(string) (*models.User, error)
FindByUsername(string) (*models.User, error)
Create(*models.User) (*models.User, error)
Update(*models.User) (*models.User, error)
Delete(string) error
}
func (r *userRepository) List() (*[]models.User, error) {
var users *[]models.User
err := r.DB.Find(&users).Error
return users, err
}
func (r *userRepository) FindByID(id string) (*models.User, error) {
var user *models.User
err := r.DB.Where("id = ?", id).First(&user).Error
return user, err
}
func (r *userRepository) FindByUsername(username string) (*models.User, error) {
var user *models.User
err := r.DB.Where("username = ?", username).First(&user).Error
return user, err
}
func (r *userRepository) Create(user *models.User) (*models.User, error) {
err := r.DB.Create(&user).Error
return user, err
}
func (r *userRepository) Update(user *models.User) (*models.User, error) {
var row *models.User
if err := r.DB.Where("id=?", user.ID).First(&row).Error; err != nil {
return nil, err
}
err := r.DB.Model(&row).Select("*").Updates(&user).Error
return row, err
}
func (r *userRepository) Delete(id string) error {
var user *models.User
if err := r.DB.Where("id=?", id).First(&user).Error; err != nil {
return err
}
err := r.DB.Delete(&user).Error
return err
}

53
internal/routers/auth.go Normal file
View File

@ -0,0 +1,53 @@
package routers
import (
"studioj/boilerplate_go/internal/controllers"
"studioj/boilerplate_go/internal/repositories"
"studioj/boilerplate_go/internal/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type AuthRouter interface {
SetRouter(db *gorm.DB, router *gin.Engine)
}
type authRouter struct {
db *gorm.DB
userRepository repositories.UserRepository
tokenRepository repositories.TokenRepository
service services.AuthService
tokenService services.TokenService
controller controllers.AuthController
router *gin.Engine
}
func InitAuthRouter(db *gorm.DB, router *gin.Engine) {
r := NewAuthRouter(db, router)
r.SetAuthRouter(db, router)
}
func NewAuthRouter(db *gorm.DB, router *gin.Engine) *authRouter {
userRepository := repositories.NewUserRepository(db)
tokenRepository := repositories.NewTokenRepository(db)
service := services.NewAuthService(userRepository, tokenRepository)
tokenService := services.NewTokenService(tokenRepository)
controller := controllers.NewAuthController(service)
return &authRouter{
db: db,
userRepository: userRepository,
tokenRepository: tokenRepository,
service: service,
tokenService: tokenService,
controller: controller,
router: router,
}
}
func (r *authRouter) SetAuthRouter(db *gorm.DB, router *gin.Engine) {
group := router.Group("/auth")
group.POST("login", r.controller.Login)
group.POST("register", r.controller.Register)
}

View File

@ -0,0 +1,19 @@
package routers
import (
"github.com/gin-gonic/gin"
"studioj/boilerplate_go/internal/database"
)
var Router *gin.Engine
func Init() {
gin.SetMode(gin.ReleaseMode)
Router = gin.Default()
maindb := database.GetDB()
InitAuthRouter(maindb, Router)
InitTokenRouter(maindb, Router)
InitUserRouter(maindb, Router)
}

46
internal/routers/token.go Normal file
View File

@ -0,0 +1,46 @@
package routers
import (
"studioj/boilerplate_go/internal/controllers"
"studioj/boilerplate_go/internal/repositories"
"studioj/boilerplate_go/internal/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type TokenRouter interface {
SetRouter(db *gorm.DB, router *gin.Engine)
}
type tokenRouter struct {
db *gorm.DB
repository repositories.TokenRepository
service services.TokenService
controller controllers.TokenController
router *gin.Engine
}
func InitTokenRouter(db *gorm.DB, router *gin.Engine) {
r := NewTokenRouter(db, router)
r.SetTokenRouter(db, router)
}
func NewTokenRouter(db *gorm.DB, router *gin.Engine) *tokenRouter {
repository := repositories.NewTokenRepository(db)
service := services.NewTokenService(repository)
controller := controllers.NewTokenController(service)
return &tokenRouter{
db: db,
repository: repository,
service: service,
controller: controller,
router: router,
}
}
func (r *tokenRouter) SetTokenRouter(db *gorm.DB, router *gin.Engine) {
// group := router.Group("/token")
// group.GET("refresh", middleware.Auth(), r.controller.Refresh)
}

50
internal/routers/user.go Normal file
View File

@ -0,0 +1,50 @@
package routers
import (
"studioj/boilerplate_go/internal/controllers"
"studioj/boilerplate_go/internal/middleware"
"studioj/boilerplate_go/internal/repositories"
"studioj/boilerplate_go/internal/services"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type UserRouter interface {
SetRouter(db *gorm.DB, router *gin.Engine)
}
type userRouter struct {
db *gorm.DB
repository repositories.UserRepository
service services.UserService
controller controllers.UserController
router *gin.Engine
}
func InitUserRouter(db *gorm.DB, router *gin.Engine) {
r := NewUserRouter(db, router)
r.SetUserRouter(db, router)
}
func NewUserRouter(db *gorm.DB, router *gin.Engine) *userRouter {
repository := repositories.NewUserRepository(db)
tokenRepository := repositories.NewTokenRepository(db)
service := services.NewUserService(repository, tokenRepository)
tokenService := services.NewTokenService(tokenRepository)
controller := controllers.NewUserController(service, tokenService)
return &userRouter{
db: db,
repository: repository,
service: service,
controller: controller,
router: router,
}
}
func (r *userRouter) SetUserRouter(db *gorm.DB, router *gin.Engine) {
group := router.Group("/user")
group.GET("/:id", middleware.Auth(), r.controller.Find)
}

90
internal/services/auth.go Normal file
View File

@ -0,0 +1,90 @@
package services
import (
"errors"
"studioj/boilerplate_go/internal/models"
"studioj/boilerplate_go/internal/repositories"
"time"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
type authService struct {
userRepository repositories.UserRepository
tokenRepository repositories.TokenRepository
}
type AuthService interface {
Register(*models.RegisterRequest) (*models.User, error)
Login(*models.LoginRequest) (*models.User, error)
CreateToken(string) (*models.Token, error)
}
func NewAuthService(userRepository repositories.UserRepository, tokenRepository repositories.TokenRepository) AuthService {
return &authService{
userRepository: userRepository,
tokenRepository: tokenRepository,
}
}
func (s *authService) Register(request *models.RegisterRequest) (*models.User, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(request.Password), 10)
if err != nil {
return nil, errors.New("fail to hash password")
}
// Create the user
newUser := models.User{
ID: uuid.NewString(),
Username: request.Username,
Password: string(hash),
}
user, err := s.userRepository.Create(&newUser)
if err != nil {
return nil, errors.New("fail to create user")
}
return user, err
}
func (s *authService) Login(request *models.LoginRequest) (*models.User, error) {
user, err := s.userRepository.FindByUsername(request.Username)
if err != nil || user == nil {
return nil, errors.New("invalid user or password")
}
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(request.Password))
if err != nil {
return nil, errors.New("invalid user or password")
}
return user, nil
}
func (s *authService) CreateToken(user_id string) (*models.Token, error) {
tokenExpiredAt := time.Now().Add(time.Hour * 24 * 30)
accessToken, err := s.tokenRepository.Generate(user_id, tokenExpiredAt.Unix())
if err != nil {
return nil, err
}
refreshExpiredAt := time.Now().Add(time.Hour * 24 * 90)
refreshToken, err := s.tokenRepository.Generate(user_id, refreshExpiredAt.Unix())
if err != nil {
return nil, err
}
newToken := &models.Token{
ID: uuid.NewString(),
UserID: user_id,
Token: accessToken,
RefreshToken: refreshToken,
Status: "valid",
ExpireAt: tokenExpiredAt,
}
token, err := s.tokenRepository.Create(newToken)
return token, err
}

115
internal/services/token.go Normal file
View File

@ -0,0 +1,115 @@
package services
import (
"fmt"
"strings"
config "studioj/boilerplate_go/configs"
"studioj/boilerplate_go/internal/models"
"studioj/boilerplate_go/internal/repositories"
"github.com/golang-jwt/jwt/v5"
)
type tokenService struct {
repository repositories.TokenRepository
}
type TokenService interface {
Find(string) (*models.Token, error)
Create(*models.Token) (*models.Token, error)
Update(*models.Token) (*models.Token, error)
Delete(string) error
Generate(string, int64) (string, error)
Verify(tokenString string) (*jwt.Token, error)
GetJwtToken(string) (*jwt.Token, error)
ExtractTokenString(string) string
VerifyTokenString(string) (*jwt.Token, error)
ValidToken(*jwt.Token) (bool, error)
}
func NewTokenService(repository repositories.TokenRepository) TokenService {
return &tokenService{
repository: repository,
}
}
func (s *tokenService) Find(id string) (*models.Token, error) {
return s.repository.Find(id)
}
func (s *tokenService) Create(token *models.Token) (*models.Token, error) {
return s.repository.Create(token)
}
func (s *tokenService) Update(token *models.Token) (*models.Token, error) {
return s.repository.Update(token)
}
func (s *tokenService) Delete(id string) error {
return s.repository.Delete(id)
}
func (s *tokenService) Verify(tokenString string) (*jwt.Token, error) {
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
}
return []byte(config.SECRET_KEY), nil
})
return jwtToken, err
}
func (s *tokenService) Generate(user_id string, expire_at int64) (string, error) {
claims := jwt.MapClaims{}
claims["authorized"] = true
claims["sub"] = user_id
claims["exp"] = expire_at
at := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
token, err := at.SignedString([]byte(config.SECRET_KEY))
return token, err
}
func (s *tokenService) GetJwtToken(tokenString string) (*jwt.Token, error) {
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
}
return []byte(config.SECRET_KEY), nil
})
return jwtToken, err
}
func (s *tokenService) ExtractTokenString(authorization string) string {
strArr := strings.Split(authorization, " ")
if len(strArr) == 2 {
return strArr[1]
}
return ""
}
func (s *tokenService) VerifyTokenString(tokenString string) (*jwt.Token, error) {
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
}
return []byte(config.SECRET_KEY), nil
})
if err != nil {
return nil, err
}
return jwtToken, nil
}
func (s *tokenService) ValidToken(jwtToken *jwt.Token) (bool, error) {
if jwtToken == nil {
return false, fmt.Errorf("no token")
}
return jwtToken.Valid, nil
}

38
internal/services/user.go Normal file
View File

@ -0,0 +1,38 @@
package services
import (
"studioj/boilerplate_go/internal/models"
"studioj/boilerplate_go/internal/repositories"
)
type userService struct {
repository repositories.UserRepository
tokenRepository repositories.TokenRepository
}
type UserService interface {
FindByID(string) (*models.User, error)
FindByUsername(string) (*models.User, error)
Create(*models.User) (*models.User, error)
}
func NewUserService(repository repositories.UserRepository, tokenRepository repositories.TokenRepository) UserService {
return &userService{
repository: repository,
tokenRepository: tokenRepository,
}
}
func (s *userService) FindByID(id string) (*models.User, error) {
return s.repository.FindByID(id)
}
func (s *userService) FindByUsername(username string) (*models.User, error) {
return s.repository.FindByUsername(username)
}
func (s *userService) Create(user *models.User) (*models.User, error) {
result, err := s.repository.Create(user)
return result, err
}