Skip to content

Commit

Permalink
先頭が大文字になっていないのを修正、言葉の修正、code-groupの使用
Browse files Browse the repository at this point in the history
  • Loading branch information
pikachu0310 committed Sep 2, 2023
1 parent 02debe0 commit 55f7b42
Show file tree
Hide file tree
Showing 2 changed files with 16 additions and 269 deletions.
273 changes: 13 additions & 260 deletions docs/chapter2/section1/2_session.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,27 +28,27 @@ func main() {
```
これらはセッションストアの設定です。

最初では、セッションの情報を記憶するための場所をデータベース上に設定しています
最初に、セッションの情報を記憶するための場所をデータベース上に設定します

この仕組みを使用するために、 `e.Use(session.Middleware(store))` を含めてセッションストアを使ってね〜、って echo に命令しています。

`e.Use(middleware.Logger())` は文字通りログを取るものです。ついでに入れましょう。

## loginHandler の実装
続いて、`loginHandler``handler.go` に実装していきましょう。
## LoginHandler の実装
続いて、`LoginHandler``handler.go` に実装していきましょう。

```go
func (h *Handler) LoginHandler(c echo.Context) error { // [!code ++]
} // [!code ++]
```
`loginHandler` の外に以下の構造体を追加します。
`LoginHandler` の外に以下の構造体を追加します。
```go
type User struct { // [!code ++]
Username string `json:"username,omitempty" db:"Username"` // [!code ++]
HashedPass string `json:"-" db:"HashedPass"` // [!code ++]
} // [!code ++]
```
`loginHandler` を実装していきます。
`LoginHandler` を実装していきます。
```go
func (h *Handler) LoginHandler(c echo.Context) error {
// リクエストを受け取り、reqに格納する // [!code ++]
Expand Down Expand Up @@ -85,7 +85,7 @@ req への代入は signUpHandler と同じです。UserName と Password が入
もしそのエラーなら 401 (Unauthorized)、そうでなければ 500 (Internal Server Error) です。
もし 404 (Not Found) とすると、「このユーザーはパスワードが違うのではなく存在しないんだ」という事がわかってしまい(このユーザーは存在していてパスワードは違う事も分かります)、セキュリティ上のリスクに繋がります。

:::info
:::tip
ここで、エラーチェックは `==` を使ってはいけません。 `errors.Is` を使いましょう。
参考: <https://pkg.go.dev/errors#Is>
:::
Expand Down Expand Up @@ -154,7 +154,7 @@ func (h *Handler) LoginHandler(c echo.Context) error {
セッションストアに登録します。
セッションの `userName` という値にそのユーザーの名前を格納していることは覚えておきましょう。

ここまで書いたら、 `loginHandler` を使えるようにしましょう。
ここまで書いたら、 `LoginHandler` を使えるようにしましょう。

```go
func main() {
Expand All @@ -170,256 +170,9 @@ func main() {
```

:::details ここまでの全体像
`main.go`
```go
package main

import (
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/srinathgs/mysqlstore"
"github.com/traPtitech/naro-template-backend/handler"
"log"
"os"
"time"

"github.com/go-sql-driver/mysql"

"github.com/jmoiron/sqlx"
"github.com/joho/godotenv"
)

func main() {
// .envファイルから環境変数を読み込み
err := godotenv.Load(".env")
if err != nil {
log.Fatal(err)
}

// データーベースの設定
jst, err := time.LoadLocation("Asia/Tokyo")
if err != nil {
log.Fatal(err)
}
conf := mysql.Config{
User: os.Getenv("DB_USERNAME"),
Passwd: os.Getenv("DB_PASSWORD"),
Net: "tcp",
Addr: os.Getenv("DB_HOSTNAME") + ":" + os.Getenv("DB_PORT"),
DBName: os.Getenv("DB_DATABASE"),
ParseTime: true,
Collation: "utf8mb4_unicode_ci",
Loc: jst,
}

// データベースに接続
db, err := sqlx.Open("mysql", conf.FormatDSN())
if err != nil {
log.Fatal(err)
}

// usersテーブルが存在しなかったら、usersテーブルを作成する
_, err = db.Exec("CREATE TABLE IF NOT EXISTS users (Username VARCHAR(255) PRIMARY KEY, HashedPass VARCHAR(255))")
if err != nil {
log.Fatal(err)
}

// セッションの情報を記憶するための場所をデータベース上に設定
store, err := mysqlstore.NewMySQLStoreFromConnection(db.DB, "sessions", "/", 60*60*24*14, []byte("secret-token"))
if err != nil {
log.Fatal(err)
}

h := handler.NewHandler(db)
e := echo.New()
e.Use(middleware.Logger()) // ログを取るミドルウェアを追加
e.Use(session.Middleware(store)) // セッション管理のためのミドルウェアを追加

e.POST("/signup", h.SignUpHandler)
e.POST("/login", h.LoginHandler)

e.GET("/cities/:cityName", h.GetCityInfoHandler)
e.POST("/cities", h.PostCityHandler)

err = e.Start(":8080")
if err != nil {
log.Fatal(err)
}
}
```
`handler.go`
```go
package handler

import (
"database/sql"
"errors"
"fmt"
"github.com/jmoiron/sqlx"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
"log"
"net/http"
)

type Handler struct {
db *sqlx.DB
}

func NewHandler(db *sqlx.DB) *Handler {
return &Handler{db: db}
}

type City struct {
ID int `json:"id,omitempty" db:"ID"`
Name sql.NullString `json:"name,omitempty" db:"Name"`
CountryCode sql.NullString `json:"countryCode,omitempty" db:"CountryCode"`
District sql.NullString `json:"district,omitempty" db:"District"`
Population sql.NullInt64 `json:"population,omitempty" db:"Population"`
}

type LoginRequestBody struct {
Username string `json:"username,omitempty" form:"username"`
Password string `json:"password,omitempty" form:"password"`
}

type User struct {
Username string `json:"username,omitempty" db:"Username"`
HashedPass string `json:"-" db:"HashedPass"`
}

func (h *Handler) SignUpHandler(c echo.Context) error {
// リクエストを受け取り、reqに格納する
req := LoginRequestBody{}
err := c.Bind(&req)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "bad request body")
}

// バリデーションする(PasswordかUsernameが空文字列の場合は400 BadRequestを返す)
if req.Password == "" || req.Username == "" {
return c.String(http.StatusBadRequest, "Username or Password is empty")
}

// 登録しようとしているユーザーが既にデータベース内に存在するかチェック
var count int
err = h.db.Get(&count, "SELECT COUNT(*) FROM users WHERE Username=?", req.Username)
if err != nil {
log.Println(err)
return c.NoContent(http.StatusInternalServerError)
}
// 存在したら409 Conflictを返す
if count > 0 {
return c.String(http.StatusConflict, "Username is already used")
}

// パスワードをハッシュ化する
hashedPass, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
// ハッシュ化に失敗したら500 InternalServerErrorを返す
if err != nil {
log.Println(err)
return c.NoContent(http.StatusInternalServerError)
}

// ユーザーを登録する
_, err = h.db.Exec("INSERT INTO users (Username, HashedPass) VALUES (?, ?)", req.Username, hashedPass)
// 登録に失敗したら500 InternalServerErrorを返す
if err != nil {
log.Println(err)
return c.NoContent(http.StatusInternalServerError)
}
// 登録に成功したら201 Createdを返す
return c.NoContent(http.StatusCreated)
}

func (h *Handler) LoginHandler(c echo.Context) error {
// リクエストを受け取り、reqに格納する
var req LoginRequestBody
err := c.Bind(&req)
if err != nil {
return c.String(http.StatusBadRequest, "bad request body")
}

// バリデーションする(PasswordかUsernameが空文字列の場合は400 BadRequestを返す)
if req.Password == "" || req.Username == "" {
return c.String(http.StatusBadRequest, "Username or Password is empty")
}

// データベースからユーザーを取得する
user := User{}
err = h.db.Get(&user, "SELECT * FROM users WHERE username=?", req.Username)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.NoContent(http.StatusUnauthorized)
} else {
log.Println(err)
return c.NoContent(http.StatusInternalServerError)
}
}
// パスワードが一致しているかを確かめる
err = bcrypt.CompareHashAndPassword([]byte(user.HashedPass), []byte(req.Password))
if err != nil {
if errors.Is(err, bcrypt.ErrMismatchedHashAndPassword) {
return c.NoContent(http.StatusUnauthorized)
} else {
return c.NoContent(http.StatusInternalServerError)
}
}

// セッションストアに登録する
sess, err := session.Get("sessions", c)
if err != nil {
fmt.Println(err)
return c.String(http.StatusInternalServerError, "something wrong in getting session")
}
sess.Values["userName"] = req.Username
sess.Save(c.Request(), c.Response())

return c.NoContent(http.StatusOK)
}

func (h *Handler) GetCityInfoHandler(c echo.Context) error {
cityName := c.Param("cityName")

var city City
err := h.db.Get(&city, "SELECT * FROM city WHERE Name=?", cityName)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return c.NoContent(http.StatusNotFound)
}
fmt.Printf("failed to get city data: %s\n", err)
return c.NoContent(http.StatusInternalServerError)
}

return c.JSON(http.StatusOK, city)
}

func (h *Handler) PostCityHandler(c echo.Context) error {
var city City
err := c.Bind(&city)
if err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "bad request body")
}

result, err := h.db.Exec("INSERT INTO city (Name, CountryCode, District, Population) VALUES (?, ?, ?, ?)", city.Name, city.CountryCode, city.District, city.Population)
if err != nil {
fmt.Printf("failed to insert city data: %s\n", err)
return c.NoContent(http.StatusInternalServerError)
}

id, err := result.LastInsertId()
if err != nil {
fmt.Printf("failed to get last insert id: %s\n", err)
return c.NoContent(http.StatusInternalServerError)
}
city.ID = int(id)

return c.JSON(http.StatusCreated, city)
}

```
::: code-group
<<<@/chapter2/section1/src/2_session/main.go{go:line-numbers}[main.go]
<<<@/chapter2/section1/src/2_session/handler.go{go:line-numbers}[handler.go]
:::

## userAuthMiddleware の実装
Expand Down Expand Up @@ -483,9 +236,9 @@ func main() {

これで、この章の目標である「ログインしないと利用できないようにする」が達成されました。

## getMeHandler の実装
## GetMeHandler の実装

最後に、 `getMeHandler` を実装します。叩いたときに自分の情報が返ってくるエンドポイントです。
最後に、 `GetMeHandler` を実装します。叩いたときに自分の情報が返ってくるエンドポイントです。

以下を `handler.go` に追加しましょう。
```go
Expand All @@ -504,7 +257,7 @@ func GetMeHandler(c echo.Context) error { // [!code ++]
アクセスしているユーザーの`userName`をセッションから取得して返しています。
`userAuthMiddleware` を実行したあとなので、`c.Get("userName").(string)` によって userName を取得できます。

`main.go``withAuth.GET("/me", getMeHandler)`を追加しましょう。
`main.go``withAuth.GET("/me", handler.GetMeHandler)`を追加しましょう。
```go
func main() {
(省略)
Expand Down
12 changes: 3 additions & 9 deletions docs/chapter2/section1/3_verify.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,9 @@
まずは完成形です。

:::details 完成形

main.go

<<<@/chapter2/section1/src/final/main.go{go:line-numbers}

handler.go

<<<@/chapter2/section1/src/final/handler.go{go:line-numbers}

::: code-group
<<<@/chapter2/section1/src/final/main.go{go:line-numbers}[main.go]
<<<@/chapter2/section1/src/final/handler.go{go:line-numbers}[handler.go]
:::

## 検証
Expand Down

0 comments on commit 55f7b42

Please sign in to comment.