こんにちは、KOUKIです。
GolangとReactでMovie Appの開発をしています。
今回は、JWT Tokenのチェック処理を実装します。
尚、Udemyの「Working with React and Go (Golang)」を参考にしているので、よかったら受講してみてください。
前回
フロントエンド側(React)でJWT Tokenのハンドリング処理を実装しました。
事前準備
フォルダ/ファイル
1 2 |
touch go-movies/src/components/Home.css mkdir go-movies/src/images |
imagesフォルダ配下に「movie_tickets.jpg」ファイルを格納します。

モジュール
1 2 |
cd backend-app/ go get github.com/justinas/alice |
JWT Tokenの存在チェック
現状、未ログインの場合はadminページへつながるリンクは画面上に表示されません。

しかし、リンク(http://localhost:3000/admin)に直接アクセスすると、画面が表示されてしまします。

この問題を修正しましょう。
TokenのString化
現在のTokenはByte型で返却されてしまっているので、String型に変換します。
1 2 3 4 5 6 7 8 |
// cmd/api/tokens.go ... func (app *application) Signin(w http.ResponseWriter, r *http.Request) { ... app.writeJSON(w, http.StatusOK, string(jwtBytes), "response") // stringで囲む } |
Tokenチェック
Tokenをチェックするミドルウェアを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
// cmd/api/middleware.go package main import ( "errors" "log" "net/http" "strconv" "strings" "time" "github.com/pascaldekloe/jwt" ) ... func (app *application) checkToken(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Vary", "Authorization") authHeader := r.Header.Get("Authorization") if authHeader == "" { // could set an anonymous user } headerParts := strings.Split(authHeader, " ") if len(headerParts) != 2 { app.errorJSON(w, errors.New("invalid auth header")) return } if headerParts[0] != "Bearer" { app.errorJSON(w, errors.New("unauthorized - no bearer")) return } token := headerParts[1] claims, err := jwt.HMACCheck([]byte(token), []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("unauthorized - failed hmac check"), http.StatusForbidden) return } if !claims.Valid(time.Now()) { app.errorJSON(w, errors.New("unauthorized - token expired"), http.StatusForbidden) return } if !claims.AcceptAudience("mydomain.com") { app.errorJSON(w, errors.New("unauthorized - invalid audience"), http.StatusForbidden) return } if claims.Issuer != "mydomain.com" { app.errorJSON(w, errors.New("unauthorized - invalid issuer"), http.StatusForbidden) return } userID, err := strconv.ParseInt(claims.Subject, 10, 64) if err != nil { app.errorJSON(w, errors.New("unauthorized"), http.StatusForbidden) return } log.Println("Valid user:", userID) next.ServeHTTP(w, r) }) } |
かなり長いですが、定型文として覚えていただければ幸いです^^;
errorJSONメソッドの修正
statusコードを受け取れるようにerrorJSONメソッドを修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// cmd/api/utilities.go ... func (app *application) errorJSON(w http.ResponseWriter, err error, status ...int) { statusCode := http.StatusBadRequest if len(status) > 0 { statusCode = status[0] } type jsonError struct { Message string `json:"message"` } theErr := jsonError{ Message: err.Error(), } app.writeJSON(w, statusCode, theErr, "error") } |
ハンドラーに混ぜる
Tokenのチェック結果をハンドラーに混ぜる処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// cmd/api/routes.go package main import ( "context" "net/http" "github.com/julienschmidt/httprouter" "github.com/justinas/alice" ) func (app *application) wrap(next http.Handler) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ctx := context.WithValue(r.Context(), "params", ps) next.ServeHTTP(w, r.WithContext(ctx)) } } func (app *application) routes() http.Handler { router := httprouter.New() secure := alice.New(app.checkToken) ... router.POST("/v1/admin/editmovie", app.wrap(secure.ThenFunc(app.editMovie))) // router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie) ... } |
justinas/aliceパッケージは、handler簡単に繋げることができるパッケージです。これで、Tokenのチェック結果をハンドラー内に混ぜることができるようになりました。
CORSヘッダーの修正
CORSヘッダーに「Authorization」を追加しましょう。
1 2 3 4 5 6 7 8 9 10 |
// cmd/api/routes.go package main ... func (app *application) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization") next.ServeHTTP(w, r) }) } |
検証1: 変更リクエストを送ってみる
ブラウザから「http://localhost:3000/admin/movie/4」へアクセスしSaveボタンを押下しましょう。
※ログインしていない場合は、ログインしてください。
※「docker-compose up」でコンテナが立ち上がります

「invalid auth header」になりました。理由は以下の2つです。
- EditMovieコンポーネントにはJWTトークンを渡していない
- Edit APIへのリクエストにJWTを渡していない
前回、JWTトークンをRedux Storeに保存する処理を実装したので、そこから取り出してリクエストヘッダーに混ぜましょう。
Redux Storeへのコネクト
EditMovieコンポーネントをRedux Storeへ接続します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// components/EditMovie.tsx import { Dispatch, Fragment, useEffect, useState, SyntheticEvent } from "react" import { connect } from 'react-redux' import { setJWTAction } from '../redux/actions/setJWTAction' const EditMovie = (props: any) => { ... } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(EditMovie) |
これで、「props.jwt」からJWTトークンを取得できます。
Save処理の変更
Saveボタンを押下してAPIへリクエストを飛ばす際、header内にJWTトークンを含めるように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// components/EditMovie.tsx ... const EditMovie = (props: any) => { ... const submit = async (e: any) => { ... const config = { // config設定 headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${props.jwt}` } } await axios.post('admin/editmovie', JSON.stringify(movie), config) // config追加 ... } ... return ( ... ) } ... |
検証2: JWTトークン付きでリクエストを送ってみる
検証1と同様の方法で、リクエストを送信してみましょう。
今度は大丈夫でしたね^^
リダイレクト処理
さて、前置きが長くなりましたが、いよいよ本題です。
JWTトークンをチェックし、ログインしていない場合は、ログイン画面にリダイレクトする処理を実装します。
EditMovieコンポーネント
useEffectはページが読み込まれたときに実行されるので、ここにJWTトークン判定を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// components/EditMovie.tsx ... const EditMovie = (props: any) => { ... useEffect(() => { if (props.jwt === "") { props.history.push({ pathname: "/login" }) return } ... } |
確認してみましょう。
未ログインの状態から「http://localhost:3000/admin/movie/4」にアクセスします。
成功ですね。
他のコンポーネントにも同様に追加します。
Adminコンポーネント
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// components/Admin.tsx import { Dispatch, Fragment, useEffect, useState } from "react" import { connect } from 'react-redux' import { setJWTAction } from '../redux/actions/setJWTAction' ... // props追加 const Admin = (props:any) => { ... useEffect(() => { if (props.jwt === "") { props.history.push({ pathname: "/login" }) ... } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(Admin) |
この状態で「http://localhost:3000/admin」にアクセスすると、下記のエラーが発生します。
「TypeError: Cannot read property ‘push’ of undefined」

そこで、App.tsxのAdmin呼び出しを変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// src/App.tsx ... const App = (props: any) => { ... return ( ... <Route path="/admin" component={Admin} /> {/* <Route path="/admin"> <Admin /> </Route> */} ... </Router> ) } |
Appの子コンポーネントとしてAdminを登録することで、pushが使えるようになります。
JWTトークンの保存
ログイン後、別のURL「https://selfnote.work/」にアクセスし、また「http://localhost:3000」に戻るとログイン状態が解除されます。
ログイン状態が解除されるのはログアウト処理を実行したときのみにしたいので、修正しましょう。
localStorage
ブラウザにデータを保存する仕組みの一つにlocalStorageがあります。
これを使って、JWTトークンをブラウザ上に記憶させましょう。
まずは保存処理です。これはsetItemメソッドを使えば簡単です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// components/Login.tsx ... const Login = (props: any) => { ... await axios.post("signin", JSON.stringify(payload)) .then((response) => { const jwtToken = response.data.response props.setJWT(jwtToken) window.localStorage.setItem( "jwt", JSON.stringify(jwtToken)) props.history.push({ pathname: "/admin" }) }) .catch((err) => { ... }) } ... return () } |
設定したJWTトークンはブラウザの開発者ツールの「Application」タブから確認可能です。

ログアウト処理の修正
ログアウト時には、localStorageに設定したJWTトークンを削除します。
削除には、removeItemメソッドを使います。
1 2 3 4 5 6 7 8 9 10 |
// src/App.tsx ... const App = (props: any) => { const logout = () => { props.setJWT("") window.localStorage.removeItem("jwt") } ... } |
ブラウザからログアウトをするとJWTトークンが削除されていることがわかります。

jwtトークンの設定
localStorageにJWTトークンがあれば、それをRedux Storeに保存する処理を追加します。
このようにしておけば、localStorageにJWTトークンが保存されている限り、ログイン状態を保つことが可能です。
getItemメソッドを使って、localStorageからJWTトークンを取得しましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// src/App.tsx ... const App = (props: any) => { useEffect(() => { const jwtToken = window.localStorage.getItem("jwt") if (jwtToken) { if (props.jwt === "") { props.setJWT(JSON.parse(jwtToken)) } } }) ... } |
OKですね。
ホームページのリファクタリング
ホームページのリファクタリングをしてこの記事は完了です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// components/Home.tsx import { Fragment } from "react" import Ticket from "./../images/movie_tickets.jpg" import "./Home.css" const Home = () => { return ( <Fragment> <div className="text-center"> <h2>This is the home page</h2> <hr /> <img src={Ticket} alt="movie ticket" /> <hr /> <div className="tickets"></div> </div> </Fragment> ) } export default Home |
1 2 3 4 5 6 7 8 |
/* components/Home.css */ .tickets { background-image: url("./../images/movie_tickets.jpg"); width: 200px; height: 143px; margin-left: auto; margin-right: auto; } |

次回
次回は、GraphQLを実装します。
記事まとめ
参考書籍
ソースコード
middleware.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
// cmd/api/middleware.go package main import ( "errors" "log" "net/http" "strconv" "strings" "time" "github.com/pascaldekloe/jwt" ) func (app *application) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type,Authorization") next.ServeHTTP(w, r) }) } func (app *application) checkToken(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Vary", "Authorization") authHeader := r.Header.Get("Authorization") if authHeader == "" { // could set an anonymous user } headerParts := strings.Split(authHeader, " ") if len(headerParts) != 2 { app.errorJSON(w, errors.New("invalid auth header")) return } if headerParts[0] != "Bearer" { app.errorJSON(w, errors.New("unauthorized - no bearer")) return } token := headerParts[1] claims, err := jwt.HMACCheck([]byte(token), []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("unauthorized - failed hmac check"), http.StatusForbidden) return } if !claims.Valid(time.Now()) { app.errorJSON(w, errors.New("unauthorized - token expired"), http.StatusForbidden) return } if !claims.AcceptAudience("mydomain.com") { app.errorJSON(w, errors.New("unauthorized - invalid audience"), http.StatusForbidden) return } if claims.Issuer != "mydomain.com" { app.errorJSON(w, errors.New("unauthorized - invalid issuer"), http.StatusForbidden) return } userID, err := strconv.ParseInt(claims.Subject, 10, 64) if err != nil { app.errorJSON(w, errors.New("unauthorized"), http.StatusForbidden) return } log.Println("Valid user:", userID) next.ServeHTTP(w, r) }) } |
routes.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
// cmd/api/routes.go package main import ( "context" "net/http" "github.com/julienschmidt/httprouter" "github.com/justinas/alice" ) func (app *application) wrap(next http.Handler) httprouter.Handle { return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { ctx := context.WithValue(r.Context(), "params", ps) next.ServeHTTP(w, r.WithContext(ctx)) } } func (app *application) routes() http.Handler { router := httprouter.New() secure := alice.New(app.checkToken) router.HandlerFunc(http.MethodGet, "/status", app.statusHandler) router.HandlerFunc(http.MethodPost, "/v1/signin", app.Signin) router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) router.HandlerFunc(http.MethodGet, "/v1/movies", app.getAllMovie) router.HandlerFunc(http.MethodGet, "/v1/movies/:genre_id", app.getAllMoviesByGenres) router.HandlerFunc(http.MethodGet, "/v1/genres", app.getAllGenres) router.POST("/v1/admin/editmovie", app.wrap(secure.ThenFunc(app.editMovie))) // router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie) router.HandlerFunc(http.MethodGet, "/v1/admin/deletemovie/:id", app.deleteMovie) return app.enableCORS(router) } |
tokens.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// cmd/api/tokens.go package main import ( "backend/models" "encoding/json" "errors" "fmt" "net/http" "time" "github.com/pascaldekloe/jwt" "golang.org/x/crypto/bcrypt" ) // tmp user var validUser = models.User{ ID: 10, Email: "me@here.com", // https://play.golang.org/p/uKMMCzJWGsW Password: "$2a$12$p3wO8E4CQOrvqcYmFMf.LeyzgxrFcK76jo1DqzEtGK9OHwsjPL1xe", } // 認証情報 type Credentials struct { Username string `json:"email"` Password string `json:"password"` } func (app *application) Signin(w http.ResponseWriter, r *http.Request) { var creds Credentials err := json.NewDecoder(r.Body).Decode(&creds) if err != nil { app.errorJSON(w, errors.New("unauthorized")) return } hashedPassword := validUser.Password // パスワードが合っているかチェック err = bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(creds.Password)) if err != nil { app.errorJSON(w, errors.New("unauthorized")) return } // JWT作成 var claims jwt.Claims // Tokenに含めたい情報を設定する claims.Subject = fmt.Sprint(validUser.ID) claims.Issued = jwt.NewNumericTime(time.Now()) claims.NotBefore = jwt.NewNumericTime(time.Now()) claims.Expires = jwt.NewNumericTime(time.Now().Add(24 * time.Hour)) claims.Issuer = "mydomain.com" claims.Audiences = []string{"mydomain.com"} jwtBytes, err := claims.HMACSign(jwt.HS256, []byte(app.config.jwt.secret)) if err != nil { app.errorJSON(w, errors.New("error signing")) return } app.writeJSON(w, http.StatusOK, string(jwtBytes), "response") } |
utilities.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// cmd/api/utilities.go package main import ( "encoding/json" "net/http" ) func (app *application) writeJSON( w http.ResponseWriter, status int, data interface{}, wrap string) error { wrapper := make(map[string]interface{}) wrapper[wrap] = data js, err := json.Marshal(wrapper) if err != nil { return err } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write(js) return nil } func (app *application) errorJSON(w http.ResponseWriter, err error, status ...int) { statusCode := http.StatusBadRequest if len(status) > 0 { statusCode = status[0] } type jsonError struct { Message string `json:"message"` } theErr := jsonError{ Message: err.Error(), } app.writeJSON(w, statusCode, theErr, "error") } |
App.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 |
// src/App.tsx import { Dispatch, Fragment, useEffect } from 'react' import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { connect } from 'react-redux' import { setJWTAction } from './redux/actions/setJWTAction' import Home from './components/Home' import Movies from './components/Movies' import Admin from './components/Admin' import Genres from './components/Genres' import OneMovie from './components/OneMovie' import OneGenre from './components/OneGenre' import EditMovie from './components/EditMovie' import Login from './components/Login' const App = (props: any) => { useEffect(() => { const jwtToken = window.localStorage.getItem("jwt") if (jwtToken) { if (props.jwt === "") { props.setJWT(JSON.parse(jwtToken)) } } }) const logout = () => { props.setJWT("") window.localStorage.removeItem("jwt") } let loginLink if (props.jwt === "") { loginLink = <Link to="/login">Login</Link> } else { loginLink = <Link to="/logout" onClick={logout}>Logout</Link> } return ( <Router> <div className="container"> <div className="row"> <div className="col mt-3"> <h1 className="mt-3"> Go Watch a Movie! </h1> </div> <div className="col mt-3 text-end"> {loginLink} </div> <hr className="mb-3" /> </div> <div className="row"> <div className="col-md-2"> <nav> <ul className="list-group"> <li className="list-group-item"> <Link to="/">Home</Link> </li> <li className="list-group-item"> <Link to="/movies">Movies</Link> </li> <li className="list-group-item"> <Link to="/genres">Genres</Link> </li> { props.jwt !== "" && <Fragment> <li className="list-group-item"> <Link to="/admin/movie/0">Add Movie</Link> </li> <li className="list-group-item"> <Link to="/admin">Manage Catalog</Link> </li> </Fragment> } </ul> </nav> </div> <div className="col-md-10"> <Switch> <Route path="/movies/:id" component={OneMovie} /> <Route path="/movies"> <Movies /> </Route> <Route path="/genre/:id" component={OneGenre} /> <Route exact path="/login" component={(props: any) => <Login {...props} />} /> <Route exact path="/genres"> <Genres /> </Route> <Route path="/admin/movie/:id" component={EditMovie} /> <Route path="/admin" component={Admin} /> <Route path="/"> <Home /> </Route> </Switch> </div> </div> </div> </Router> ) } // State -> Props const mapStateToProps = (state: {jwt: string}) => ({ jwt: state.jwt }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) // AppコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(App) |
App.tsx
Admin.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
// components/Admin.tsx import { Dispatch, Fragment, useEffect, useState } from "react" import { connect } from 'react-redux' import { setJWTAction } from '../redux/actions/setJWTAction' import { Movie } from '../models/movie' import { Link } from 'react-router-dom' import axios from 'axios' const Admin = (props: any) => { const [movies, setMovies] = useState<Movie[]>([]) const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") useEffect(() => { if (props.jwt === "") { props.history.push({ pathname: "/login" }) return } ( async () => { await axios.get('movies') .then((response) => { setMovies(response.data.movies) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } )() }, []) if (error) { return ( <div>Error: {error}</div> ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Manage Catalogue</h2> <hr /> <div className="list-group"> {movies.map((m) => { return ( <Link key={m.id} to={`/admin/movie/${m.id}`} className="list-group-item list-group-item-action" > {m.title} </Link> ) })} </div> </Fragment> ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(Admin) |
EditMovie.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
// components/EditMovie.tsx import { Dispatch, Fragment, useEffect, useState, SyntheticEvent } from "react" import { connect } from 'react-redux' import { setJWTAction } from '../redux/actions/setJWTAction' import { AlertType } from '../models/ui-components' import { Movie } from '../models/movie' import { Link } from 'react-router-dom' import { confirmAlert } from 'react-confirm-alert' import 'react-confirm-alert/src/react-confirm-alert.css' import Input from './form-components/Input' import TextArea from './form-components/TextArea' import Select from './form-components/Select' import Alert from './ui-components/Alert' import './EditMovie.css' import axios from 'axios' const EditMovie = (props: any) => { const [movie, setMovie] = useState<Movie>() const [isLoaded, setIsLoaded] = useState(false) const [error, setError] = useState("") const [errors, setErrors] = useState<string[]>([]) const [alert, setAlert] = useState<AlertType>({type: "d-none", message: ""}) // form data const [id, setID] = useState(0) const [title, setTitle] = useState("") const [description, setDescription] = useState("") const [releaseDate, setReleaseDate] = useState("") const [runtime, setRuntime] = useState(0) const [rating, setRating] = useState(0) const [mpaaRating, setMpaaRating] = useState("") const [mpaaOptions, setMpaaOptions] = useState([{}]) useEffect(() => { if (props.jwt === "") { props.history.push({ pathname: "/login" }) return } ( async () => { setMpaaOptions([ {id: "G", value: "G"}, {id: "PG", value: "PG"}, {id: "PG13", value: "PG13"}, {id: "R", value: "R"}, {id: "NC17", value: "NC17"}, ]) // URLからIDを取得 setID(props.match.params.id) if (id > 0) { // Edit await axios.get(`movie/${id}`) .then((response) => { let movie = response.data.movie const releaseDate = new Date(movie.release_date) movie.release_date = releaseDate.toISOString().split("T")[0] setMovie(movie) setTitle(movie.title) setDescription(movie.description) setReleaseDate(movie.release_date) setRuntime(movie.runtime) setRating(movie.rating) setMpaaRating(movie.mpaa_rating) setIsLoaded(true) }) .catch((err) => { setError(err.message) }) } else { // New setIsLoaded(true) } } )() }, [id]) const submit = async (e: any) => { e.preventDefault() const movie: Movie = { id: id, title: title, description: description, year: "2021", release_date: releaseDate, runtime: Number(runtime), rating: Number(rating), mpaa_rating: mpaaRating, genres: [] } // validation let localErrors: string[] = [] if (movie?.title === "") { localErrors.push("title") } if (movie?.release_date === "") { localErrors.push("release_date") } if (movie?.runtime === 0) { localErrors.push("runtime") } if (movie?.rating === 0) { localErrors.push("rating") } setErrors(localErrors) if (errors.length > 0) { return false } const config = { headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${props.jwt}` } } await axios.post('admin/editmovie', JSON.stringify(movie), config) .then((response) => { setAlert({ type: 'alert-success', message: 'Changes saved!' }) props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.message) setAlert({ type: 'alert-danger', message: err.response.data.error.message }) }) } const hasError = (key: string) => { return errors.indexOf(key) !== -1 } const confirmDelete = (e: SyntheticEvent) => { e.preventDefault() confirmAlert({ title: 'Delete Movie?', message: 'Are you sure?', buttons: [ { label: 'Yes', onClick: async () => { await axios.get(`admin/deletemovie/${id}`) .then((response) => { props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.message) setAlert({ type: 'alert-danger', message: err.response.data.error.message }) }) } }, { label: 'No', onClick: () => {} } ] }); } if (error) { // return ( // <div>Error: {error}</div> // ) } else if (!isLoaded) { return ( <p>Loading...</p> ) } return ( <Fragment> <h2>Add/Edit Movie</h2> <Alert alertType={alert.type} alertMessage={alert.message} /> <hr /> <form method="post" onSubmit={submit}> <Input title={"Title"} className={hasError("title") ? "is-invalid" : ""} type={'text'} name={'title'} value={title} handleChange={setTitle} errorDiv={hasError("title") ? "text-danger" : "d-none"} errorMsg={"Please enter a title"} /> <Input title={"Release Date"} className={hasError("release_date") ? "is-invalid" : ""} type={'date'} name={'release_date'} value={releaseDate} handleChange={setReleaseDate} errorDiv={hasError("release_date") ? "text-danger" : "d-none"} errorMsg={"Please enter a release_date"} /> <Input title={"Runtime"} className={hasError("runtime") ? "is-invalid" : ""} type={'text'} name={'runtime'} value={runtime} handleChange={setRuntime} errorDiv={hasError("runtime") ? "text-danger" : "d-none"} errorMsg={"Please enter a runtime"} /> <Select title={'MPAA Rating'} name={'mpaa_rating'} options={mpaaOptions} value={mpaaRating} handleChange={setMpaaRating} placeholder={'Choose...'} /> <Input title={"Rating"} className={hasError("rating") ? "is-invalid" : ""} type={'text'} name={'rating'} value={rating} handleChange={setRating} errorDiv={hasError("rating") ? "text-danger" : "d-none"} errorMsg={"Please enter a rating"} /> <TextArea title={"Description"} name={'description'} value={description} handleChange={setDescription} rows={3} /> <hr /> <button className="btn btn-primary" type="submit">Save</button> <Link to="/admin" className="btn btn-warning ms-1">Cancel</Link> {id > 0 && ( <a href="#!" onClick={confirmDelete} className="btn btn-danger ms-1"> Delete </a> )} </form> </Fragment> ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(EditMovie) |
Home.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// components/Home.tsx import { Fragment } from "react" import Ticket from "./../images/movie_tickets.jpg" import "./Home.css" const Home = () => { return ( <Fragment> <div className="text-center"> <h2>This is the home page</h2> <hr /> <img src={Ticket} alt="movie ticket" /> <hr /> <div className="tickets"></div> </div> </Fragment> ) } export default Home |
Home.css
1 2 3 4 5 6 7 8 9 |
/* components/Home.css */ .tickets { background-image: url("./../images/movie_tickets.jpg"); width: 200px; height: 143px; margin-left: auto; margin-right: auto; } |
Login.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
// components/Login.tsx import { Dispatch, useState, Fragment, SyntheticEvent } from 'react' import { connect } from 'react-redux' import { AlertType } from '../models/ui-components' import { Credentials } from '../models/tokens' import { setJWTAction } from '../redux/actions/setJWTAction' import Alert from './ui-components/Alert' import Input from './form-components/Input' import axios from 'axios' const Login = (props: any) => { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [error, setError] = useState("") const [errors, setErrors] = useState<string[]>([]) const [alert, setAlert] = useState<AlertType>({type: "d-none", message: ""}) const submit = async (e: SyntheticEvent) => { e.preventDefault() // エラー初期化 setErrors([]) let submitErrors: string[] = [] if (email === "") { submitErrors.push("email") } if (password === "") { submitErrors.push("password") } if (submitErrors.length > 0) { setErrors(submitErrors) return } const payload: Credentials = { username: email, password: password } await axios.post("signin", JSON.stringify(payload)) .then((response) => { const jwtToken = response.data.response props.setJWT(jwtToken) window.localStorage.setItem( "jwt", JSON.stringify(jwtToken)) props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.response.data.error.message) setAlert({ type: "alert-danger", message: err.response.data.error.message}) }) } const hasError = (key: string) => { return errors.indexOf(key) !== -1 } return ( <Fragment> <h2>Login</h2> <hr /> <Alert alertType={alert.type} alertMessage={alert.message} /> <form className="pt-3" onSubmit={submit}> <Input title={"Email"} type={'email'} name={'email'} handleChange={setEmail} className={hasError("email") ? "is-invalid" : ""} errorDiv={hasError("email") ? "text-danger" : "d-none"} errorMsg={"Please enter a valid email address"} /> <Input title={"Password"} type={'password'} name={'password'} handleChange={setPassword} className={hasError("password") ? "is-invalid" : ""} errorDiv={hasError("password") ? "text-danger" : "d-none"} errorMsg={"Please enter a password"} /> <hr /> <button className="btn btn-primary">Login</button> </form> </Fragment> ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(Login) |
tokens.ts
1 2 3 4 5 6 |
// models/tokens.ts export interface Credentials { username: string password: string } |
コメントを残す
コメントを投稿するにはログインしてください。