Skip to content
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.

feat: 認証機構 #23

Merged
merged 23 commits into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ MYSQL_ROOT_PASSWORD="test-root-pass"
MYSQL_DATABASE="test-db"
MYSQL_USER="test-taro"
MYSQL_PASSWORD="test-pass"

GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GITHUB_REDIRECT_URI="http://localhost/api/auth/callback"

JWT_SECRET="test-jwt-secret"
3 changes: 3 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ services:
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- JWT_SECRET=${JWT_SECRET}
volumes:
- ./server/public:/app/public
depends_on:
Expand Down
5 changes: 5 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,16 @@ services:
target: dev
volumes:
- ./server:/app
ports:
- 8000:8000
environment:
- MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD}
- MYSQL_DATABASE=${MYSQL_DATABASE}
- MYSQL_USER=${MYSQL_USER}
- MYSQL_PASSWORD=${MYSQL_PASSWORD}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- JWT_SECRET=${JWT_SECRET}
depends_on:
database:
condition: service_healthy
Expand Down
16 changes: 8 additions & 8 deletions etc/nginx/dev/conf.d/meline.conf
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ server {
listen 80;
server_name localhost;

location / {
proxy_pass http://client:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}

location /images {
alias /var/www/public/images;
}
Expand All @@ -21,4 +13,12 @@ server {
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}

location / {
proxy_pass http://client:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
3 changes: 1 addition & 2 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -o main .
RUN CGO_ENABLED=0 go build -o main cmd/server/main.go


FROM alpine AS prod
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/main /app/main
COPY --from=builder /app/sql /app/sql

EXPOSE 8000
ENTRYPOINT ["/app/main"]
Expand Down
2 changes: 1 addition & 1 deletion server/cmd/seeder/seeds/user_seeder.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"errors"

"github.com/uptrace/bun"
"github.com/saitamau-maximum/meline/usecase/model"
"github.com/saitamau-maximum/meline/models"
)

var (
Expand Down
25 changes: 20 additions & 5 deletions server/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@ package main
import (
"net/http"

_ "github.com/go-sql-driver/mysql"
"github.com/labstack/echo/v4"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/mysqldialect"
_ "github.com/go-sql-driver/mysql"

infra "github.com/saitamau-maximum/meline/infra/mysql"
"github.com/saitamau-maximum/meline/controller/handler"
"github.com/saitamau-maximum/meline/controller/gateway"
"github.com/saitamau-maximum/meline/infra/github"
"github.com/saitamau-maximum/meline/infra/mysql"
"github.com/saitamau-maximum/meline/usecase"
)

const (
Expand All @@ -18,17 +22,28 @@ const (
func main() {
e := echo.New()

db, err := infra.ConnectDB(HOST)
db, err := mysql.ConnectDB(HOST)
if err != nil {
e.Logger.Error(err)
}

bunDB := bun.NewDB(db, mysqldialect.New())
defer bunDB.Close()

e.GET("/", func(c echo.Context) error {
apiGroup := e.Group("/api")

oAuthConf := github.NewGithubOAuthConf()
authRepository := github.NewAuthRepository(oAuthConf)
userRepository := mysql.NewUserRepository(bunDB)
authInteractor := usecase.NewAuthInteractor(authRepository)
userInteractor := usecase.NewUserInteractor(userRepository)
authGateway := gateway.NewAuthGateway(userInteractor)

handler.NewAuthHandler(apiGroup, authInteractor, userInteractor)

apiGroup.GET("/", authGateway.Auth(func (c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
}))

e.Start(":8000")
}
Expand Down
72 changes: 72 additions & 0 deletions server/controller/gateway/auth_gateway.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package gateway

import (
"fmt"
"net/http"
"os"
"time"

jwt "github.com/dgrijalva/jwt-go"
"github.com/labstack/echo/v4"
"github.com/saitamau-maximum/meline/usecase"
)

type AuthGateway struct {
userInteractor usecase.IUserInteractor
}

func NewAuthGateway(userInteractor usecase.IUserInteractor) *AuthGateway {
return &AuthGateway{
userInteractor: userInteractor,
}
}

func (h *AuthGateway) Auth(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ctx := c.Request().Context()

// Get Access Token
cookie, err := c.Cookie("access_token")
if err != nil {
return c.JSON(http.StatusUnauthorized, err)
}

token, err := jwt.Parse(cookie.Value, func(token *jwt.Token) (interface{}, error) {
if token.Method.Alg() != "HS256" {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}

jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
return nil, fmt.Errorf("JWT_SECRET is not set")
}

return []byte(jwtSecret), nil
})
sor4chi marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return c.JSON(http.StatusUnauthorized, err)
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return c.JSON(http.StatusUnauthorized, err)
}

// Get User
userId := claims["user_id"].(float64)

user, err := h.userInteractor.GetUserByID(ctx, uint64(userId))
if err != nil {
return c.JSON(http.StatusUnauthorized, err)
}

c.Set("user", user)

exp := claims["exp"].(float64)
if int64(exp) < time.Now().Unix() {
return c.JSON(http.StatusForbidden, err)
}

return next(c)
}
}
99 changes: 99 additions & 0 deletions server/controller/handler/auth_handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package handler

import (
"context"
"database/sql"
"log"
"net/http"
"time"

"github.com/labstack/echo/v4"
"github.com/saitamau-maximum/meline/usecase"
)

const (
b = 32
)

type AuthHandler struct {
authInteractor usecase.IAuthInteractor
userInteractor usecase.IUserInteractor
}

func NewAuthHandler(apiGroup *echo.Group, authInteractor usecase.IAuthInteractor, userInteractor usecase.IUserInteractor) {
authHandler := &AuthHandler{
authInteractor: authInteractor,
userInteractor: userInteractor,
}

authGroup := apiGroup.Group("/auth")
authGroup.GET("/login", authHandler.Login)
authGroup.GET("/callback", authHandler.CallBack)
}
sor4chi marked this conversation as resolved.
Show resolved Hide resolved

func (h *AuthHandler) Login(c echo.Context) error {
ctx := c.Request().Context()

// Get Github OAuth URL
state := h.authInteractor.GenerateState(b)
url := h.authInteractor.GetGithubOAuthURL(ctx, state)

return c.Redirect(http.StatusMovedPermanently, url)
}

func (h *AuthHandler) CallBack(c echo.Context) error {
ctx := context.Background()

code := c.QueryParam("code")
gitToken, err := h.authInteractor.GetGithubOAuthToken(ctx, code)
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusUnauthorized, err)
}

res, err := h.authInteractor.GetGithubUser(ctx, gitToken)
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusUnauthorized, err)
}

// Get User
githubId := res["login"].(string)
Daaaai0809 marked this conversation as resolved.
Show resolved Hide resolved
githubName := res["name"].(string)

user, err := h.userInteractor.GetUserByGithubID(ctx, githubId)
if err != nil {
if (err == sql.ErrNoRows) {
githubImageURL := res["avatar_url"].(string)

user, err = h.userInteractor.CreateUserAndGetByGithubID(ctx, githubId, githubName, githubImageURL)
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusInternalServerError, err)
}
} else {
log.Default().Println(err)
return c.JSON(http.StatusInternalServerError, err)
}
}

// Set Access Token
token, err := h.authInteractor.CreateAccessToken(ctx, user)
if err != nil {
log.Default().Println(err)
return c.JSON(http.StatusInternalServerError, err)
}

cookie := new(http.Cookie)
cookie.Name = "access_token"
cookie.Value = token
cookie.Path = "/"
cookie.HttpOnly = true
cookie.Secure = true
cookie.SameSite = http.SameSiteLaxMode
cookie.Expires = time.Now().Add(24 * time.Hour)

c.SetCookie(cookie)

return c.JSON(http.StatusOK, "success")
}
15 changes: 8 additions & 7 deletions server/domain/entity/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ import (
)

type User struct {
ID uint64
GithubID string
Name string
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
ID uint64 `json:"id"`
GithubID string `json:"github_id"`
Name string `json:"name"`
ImageURL string `json:"image_url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt time.Time `json:"deleted_at"`
}

func NewUserEntity(id uint64, githubID, name string, createdAt, updatedAt, deletedAt time.Time) *User {
Expand All @@ -22,4 +23,4 @@ func NewUserEntity(id uint64, githubID, name string, createdAt, updatedAt, delet
UpdatedAt: updatedAt,
DeletedAt: deletedAt,
}
}
}
11 changes: 11 additions & 0 deletions server/domain/repository/auth_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package repository

import (
"context"
)

type IAuthRepository interface {
GetGithubOAuthURL(ctx context.Context, state string) string
GetGithubOAuthToken(ctx context.Context, code string) (string, error)
GetGithubUser(ctx context.Context, token string) (map[string]interface{}, error)
sor4chi marked this conversation as resolved.
Show resolved Hide resolved
}
13 changes: 13 additions & 0 deletions server/domain/repository/user_repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package repository

import (
"context"

"github.com/saitamau-maximum/meline/models"
)

type IUserRepository interface {
FindByID(ctx context.Context, id uint64) (*model.User, error)
FindByGithubID(ctx context.Context, githubID string) (*model.User, error)
Create(ctx context.Context, user *model.User) error
}
5 changes: 5 additions & 0 deletions server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@ module github.com/saitamau-maximum/meline
go 1.20

require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/go-sql-driver/mysql v1.7.1
github.com/labstack/echo/v4 v4.11.2
github.com/uptrace/bun v1.1.16
github.com/uptrace/bun/dialect/mysqldialect v1.1.16
github.com/uptrace/bun/extra/bundebug v1.1.16
github.com/urfave/cli/v2 v2.25.7
golang.org/x/oauth2 v0.13.0
)

require (
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
Expand All @@ -30,4 +33,6 @@ require (
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.31.0 // indirect
)
Loading