สร้าง JSON Web Tokens
การพัฒนาเว็บไซต์ในรูปแบบ RESTFul API ซึ่งเป็น Web Server ในรูปแบบ stateless สำหรับเป็น API ทั้ง Single Page Application และ Mobile คือไม่มีการจดจำ state ของผู้ใช้แต่ว่าใช้ token base แทน
JWT คืออะไร ?
JWT ย่อมาจาก JSON Web Token เป็นรูปแบบหนึ่งที่ใช้ในการสร้างรหัส token จากข้อมูล JSON Data แล้วทำการเข้ารหัสด้วย Base64Url Encoded เป็นมาตรฐานเปิด (RFC 7519) ที่เข้ามาแก้ปัญหาการส่งข้อมูลอย่างปลอดภัยระหว่างกัน โดยที่ถูกออกแบบไว้ว่า จะต้องมีขนาดที่กระทัดรัด (Compact) และเก็บข้อมูลภายในตัว (Self-contained)
Token คืออะไร ?
เป็นรหัสชุดนึงที่เอาไว้สำหรับทดแทน session ซึ่งเอาไว้ระบุว่าคนๆนั้นคือใคร ตัวอย่างเช่น Facebook เมื่อล็อคอินเสร็จแล้วจะมี accessToken เพื่อระบุตัวตนว่าเป็นใคร ซึ่งตัว token เอามาใช้ในการทำ RESTFul API ทดแทนการทำ Web Server แบบเดิมๆ ที่เก็บในรูปแบบ session โดยตัว token จะถูกส่งไปทุกๆ request ผ่าน HTTP Headers
ข้อกำหนดเบื้องต้น
ข้อกำหนดสำหรับบทความนี้คือ คุณต้องปฏิบัติตามบทความ เพิ่ม ลบ แก้ไข Database ด้วย React มาก่อน
JSON Web Tokens on the back end
ที่ Back-End ติดตั้ง JWT ด้วยคำสั่ง
go get github.com/pascaldekloe/jwt
เพิ่มเส้นทางใหม่ ที่ไฟล์ routes.go
package main
import (
"net/http"
"github.com/julienschmidt/httprouter"
)
func (app *application) routes() http.Handler {
router := httprouter.New()
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.getAllMovies)
router.HandlerFunc(http.MethodGet, "/v1/movies/:genre_id", app.getAllMoviesByGenre)
router.HandlerFunc(http.MethodGet, "/v1/genres", app.getAllGenres)
router.HandlerFunc(http.MethodPost, "/v1/admin/editmovie", app.editMovie)
router.HandlerFunc(http.MethodGet, "/v1/admin/deletemovie/:id", app.deleteMovie)
return app.enableCORS(router)
}
เพิ่มสตรัค User ที่ไฟล์ models.go
package models
import (
"database/sql"
"time"
)
// Models is the wrapper for database
type Models struct {
DB DBModel
}
// NewModels returns models with db pool
func NewModels(db *sql.DB) Models {
return Models{
DB: DBModel{DB: db},
}
}
// Movie is the type for movies
type Movie struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Year int `json:"year"`
ReleaseDate time.Time `json:"release_date"`
Runtime int `json:"runtime"`
Rating int `json:"rating"`
MPAARating string `json:"mpaa_rating"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
MovieGenre map[int]string `json:"genres"`
}
// Genre is the type for genre
type Genre struct {
ID int `json:"id"`
GenreName string `json:"genre_name"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
// MovieGenre is the type for movie genre
type MovieGenre struct {
ID int `json:"-"`
MovieID int `json:"-"`
GenreID int `json:"-"`
Genre Genre `json:"genre"`
CreatedAt time.Time `json:"-"`
UpdatedAt time.Time `json:"-"`
}
// User is the type for users
type User struct {
ID int
Email string
Password string
}
bcrypt
bcrypt เป็น password hashing function ที่สร้างขึ้นจากพื้นฐานของ Blowfish cipher โดยการทำงานของ Blowfish cipher ที่การสร้าง key ใหม่ขึ้นมาจะต้องทำการ pre-processโดยใช้เวลาเทียบเก่ากับการเข้ารหัสตัวอักษรขนาด 4KB
ติดตั้ง bcrypt ด้วยคำสั่ง
go get golang.org/x/crypto/bcrypt
สร้าง password แบบแฮช (bcrypt)
การเก็บรหัสผ่านในรูปแบบของ Hash หรือชื่ออย่างเป็นทางการคือ Cryptographic Hash ว่ามันเป็นการสร้างข้อมูลใหม่ขึ้นแทนข้อมูลเก่า เป็นกระบวนการที่ใช้ตัวอักษร ตัวเลข อักขระพิเศษ มารวมกันแล้วสร้างเป็นบางอย่างที่เข้าใจไม่ได้ มีความซับซ้อน และคนอ่านไม่มีทางเข้าใจ
ตัวอย่างเช่น จะตั้งรหัสผ่านเป็นคำว่า password ให้ไปที่ https://go.dev/play/p/uKMMCzJWGsW เขียนโค้ดและ Run
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
password := "password"
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), 12)
fmt.Println(string(hashedPassword))
}
รหัสผ่าน password จะแปลงเป็นรหัสแบบแฮชคือ $2a$12$hpqLR40He6h5MegJmHbLYe0OZJTVdPh32R.SKLRPgZwbvuGKr6WOi
แล้วนำ รหัสแบบแฮชนี้ ไปใส่ในโค้ดที่ Password:
สร้างไฟล์ใหม่ ชื่อ tokens.go ภายในโฟลเดอร์ api เขียนโค้ดดังนี้
package main
import (
"backend/models"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/pascaldekloe/jwt"
"golang.org/x/crypto/bcrypt"
)
var validUser = models.User{
ID: 10,
Email: "me@here.com",
Password: "$2a$12$hpqLR40He6h5MegJmHbLYe0OZJTVdPh32R.SKLRPgZwbvuGKr6WOi",
}
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
}
var claims jwt.Claims
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, jwtBytes, "reponse")
}
Secret
Secret คำนี้ใช้มากที่สุดเมื่อต้องการพูดถึงเรื่องของความลับ มีความหมายว่า ความลับ เป็นความลับ แอบ ซ่อน ปิดบัง ซ่อนเร้น Symmetric Cryptography (Secret key) คือ การเข้ารหัสและถอดรหัสโดยใช้กุญแจรหัสตัวเดียวกัน คือ ผู้ส่งและผู้รับจะต้องมีกุญแจรหัสที่เหมือนกันเพื่อใช้ในการเข้ารหัสและถอดรหัส
สร้าง JWT Secret
ไปที่ https://go.dev/play/p/s8KlqJIOWej เขียนโค้ดและ Run
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func main() {
secret := "mysecret"
data := "data"
fmt.Printf("Secret: %s Data: %s\n", secret, data)
// Create a new HMAC by defining the hash type and the key (as byte array)
h := hmac.New(sha256.New, []byte(secret))
// Write Data to it
h.Write([]byte(data))
// Get result and encode as hexadecimal string
sha := hex.EncodeToString(h.Sum(nil))
fmt.Println("Result: " + sha)
}
คำว่า mysecret จะถูกแปลงเป็นรหัสคือ 2dce505d96a53c5768052ee90f3df2055657518dad489160df9913f66042e160
แล้วนำรหัสนี้ ไปใส่ในโค้ดที่ “jwt-secret “
แก้ไขโค้ดที่ไฟล์ main.go แล้วทดสอบการทำงาน
package main
import (
"backend/models"
"context"
"database/sql"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
_ "github.com/lib/pq"
)
const version = "1.0.0"
type config struct {
port int
env string
db struct {
dsn string
}
jwt struct {
secret string
}
}
type AppStatus struct {
Status string `json:"status"`
Environment string `json:"environment"`
Version string `json:"version"`
}
type application struct {
config config
logger *log.Logger
models models.Models
}
func main() {
var cfg config
flag.IntVar(&cfg.port, "port", 4000, "Server port to listen on")
flag.StringVar(&cfg.env, "env", "development", "Application environment (development|production")
flag.StringVar(&cfg.db.dsn, "dsn", "postgres://postgres:lungmaker@localhost:54321/go_movies?sslmode=disable", "Postgres connection string")
flag.StringVar(&cfg.jwt.secret, "jwt-secret", "2dce505d96a53c5768052ee90f3df2055657518dad489160df9913f66042e160", "secret")
flag.Parse()
logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)
db, err := openDB(cfg)
if err != nil {
logger.Fatal(err)
}
defer db.Close()
app := &application{
config: cfg,
logger: logger,
models: models.NewModels(db),
}
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.port),
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
logger.Println("Starting server on port", cfg.port)
err = srv.ListenAndServe()
if err != nil {
log.Println(err)
}
}
func openDB(cfg config) (*sql.DB, error) {
db, err := sql.Open("postgres", cfg.db.dsn)
if err != nil {
return nil, err
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = db.PingContext(ctx)
if err != nil {
return nil, err
}
return db, nil
}
App to a component, and setting up state
Front-End ที่ไฟล์ App.js แก้ไขโค้ดดังนี้
import React, { Component, Fragment } from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Movies from "./components/Movies";
import Admin from "./components/Admin";
import Home from "./components/Home";
import OneMovie from "./components/OneMovie";
import Genres from "./components/Genres";
import OneGenre from "./components/OneGenre";
import EditMovie from "./components/EditMovie";
import Login from "./components/Login";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
jwt: "",
};
this.handleJWTChange(this.handleJWTChange.bind(this));
}
handleJWTChange = (jwt) => {
this.setState({ jwt: jwt });
};
logout = () => {
this.setState({ jwt: "" });
};
render() {
let loginLink;
if (this.state.jwt === "") {
loginLink = <Link to="/login">Login</Link>;
} else {
loginLink = (
<Link to="/logout" onClick={this.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"></hr>
</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>
{this.state.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 Catalogue</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) => <Login {...props} handleJWTChange={this.handleJWTChange} />} />
<Route exact path="/genres">
<Genres />
</Route>
<Route path="/admin/movie/:id" component={EditMovie} />
<Route path="/admin">
<Admin />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</div>
</div>
</Router>
);
}
}
ภายในโฟลเดอร์ components สร้างไฟล์ใหม่ชื่อ Login.js มีโค้ดดังนี้
import React, { Component, Fragment } from "react";
import Input from "./form-components/Input";
import Alert from "./ui-components/Alert";
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: "",
password: "",
error: null,
errors: [],
alert: {
type: "d-none",
message: "",
},
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange = (evt) => {
let value = this.target.value;
let name = this.target.name;
this.setState((prevState) => ({
...prevState,
[name]: value,
}));
};
handleSubmit = (evt) => {
evt.preventDefault();
};
hasError(key) {
return this.state.errors.indexOf(key) !== -1;
}
render() {
return (
<Fragment>
<h2>Login</h2>
<hr />
<Alert
alertType={this.state.alert.type}
alertMessage={this.state.alert.message}
/>
<form className="pt-3" onSubmit={this.handleSubmit}>
<Input
title={"Email"}
type={"email"}
name={"email"}
handleChange={this.handleChange}
className={this.hasError("email") ? "is-invalid" : ""}
errorDiv={this.hasError("email") ? "text-danger" : "d-none"}
errorMsg={"Please enter a valid email address"}
/>
<Input
title={"Password"}
type={"password"}
name={"password"}
handleChange={this.handleChange}
className={this.hasError("password") ? "is-invalid" : ""}
errorDiv={this.hasError("password") ? "text-danger" : "d-none"}
errorMsg={"Please enter a password"}
/>
<hr />
<button className="btn btn-primary">Login</button>
</form>
</Fragment>
);
}
}
ที่เว็บเบราเซอร์ ไปที่ http://localhost:3000/login
Handling Login
แก้ไขไฟล์ Login.js มีโค้ดดังนี้
import React, { Component, Fragment } from "react";
import Input from "./form-components/Input";
import Alert from "./ui-components/Alert";
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: "",
password: "",
error: null,
errors: [],
alert: {
type: "d-none",
message: "",
},
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange = (evt) => {
let value = evt.target.value;
let name = evt.target.name;
this.setState((prevState) => ({
...prevState,
[name]: value,
}));
};
handleSubmit = (evt) => {
evt.preventDefault();
let errors = [];
if (this.state.email === "") {
errors.push("email");
}
if (this.state.password === "") {
errors.push("password");
}
this.setState({errors: errors});
if (errors.length > 0) {
return false;
}
const data = new FormData(evt.target);
const payload = Object.fromEntries(data.entries());
const requestOptions = {
method: "POST",
body: JSON.stringify(payload),
}
fetch("http://localhost:4000/v1/signin", requestOptions)
.then((response) => response.json())
.then((data) => {
if (data.error) {
this.setState({
alert: {
type: "alert-danger",
message: data.error.message,
}
})
} else {
console.log(data);
}
})
};
hasError(key) {
return this.state.errors.indexOf(key) !== -1;
}
render() {
return (
<Fragment>
<h2>Login</h2>
<hr />
<Alert
alertType={this.state.alert.type}
alertMessage={this.state.alert.message}
/>
<form className="pt-3" onSubmit={this.handleSubmit}>
<Input
title={"Email"}
type={"email"}
name={"email"}
handleChange={this.handleChange}
className={this.hasError("email") ? "is-invalid" : ""}
errorDiv={this.hasError("email") ? "text-danger" : "d-none"}
errorMsg={"Please enter a valid email address"}
/>
<Input
title={"Password"}
type={"password"}
name={"password"}
handleChange={this.handleChange}
className={this.hasError("password") ? "is-invalid" : ""}
errorDiv={this.hasError("password") ? "text-danger" : "d-none"}
errorMsg={"Please enter a password"}
/>
<hr />
<button className="btn btn-primary">Login</button>
</form>
</Fragment>
);
}
}
ที่เว็บเบราเซอร์ ไปที่ http://localhost:3000/login ใส่ Email : me@here.com และ Password ที่ไม่ถูกต้อง เช่น aaaa จะแสดง ข้อความเตือน unauthorized
Adding middleware to check for a valid token
ที่ Back-End ติดตั้ง alice ด้วยคำสั่ง
go get github.com/justinas/alice
ไฟล์ tokens.go แก้ไขโค้ดดังนี้
package main
import (
"backend/models"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/pascaldekloe/jwt"
"golang.org/x/crypto/bcrypt"
)
var validUser = models.User{
ID: 10,
Email: "me@here.com",
Password: "$2a$12$hpqLR40He6h5MegJmHbLYe0OZJTVdPh32R.SKLRPgZwbvuGKr6WOi",
}
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
}
var claims jwt.Claims
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), "reponse")
}
ไฟล์ 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"))
return
}
if !claims.Valid(time.Now()) {
app.errorJSON(w, errors.New("unauthorized - token expired"))
return
}
if !claims.AcceptAudience("mydomain.com") {
app.errorJSON(w, errors.New("unauthorized - invalid audience"))
return
}
if claims.Issuer != "mydomain.com" {
app.errorJSON(w, errors.New("unauthorized - invalid issuer"))
return
}
userID, err := strconv.ParseInt(claims.Subject, 10, 64)
if err != nil {
app.errorJSON(w, errors.New("unauthorized"))
return
}
log.Println("Valid user:", userID)
next.ServeHTTP(w, r)
})
}
ไฟล์ 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.getAllMovies)
router.HandlerFunc(http.MethodGet, "/v1/movies/:genre_id", app.getAllMoviesByGenre)
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)
}
Adding redirects for protected components
Front-End ที่ไฟล์ App.js แก้ไขโค้ดดังนี้
import React, { Component, Fragment } from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import Movies from "./components/Movies";
import Admin from "./components/Admin";
import Home from "./components/Home";
import OneMovie from "./components/OneMovie";
import Genres from "./components/Genres";
import OneGenre from "./components/OneGenre";
import EditMovie from "./components/EditMovie";
import Login from "./components/Login";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
jwt: "",
};
this.handleJWTChange(this.handleJWTChange.bind(this));
}
handleJWTChange = (jwt) => {
this.setState({ jwt: jwt });
};
logout = () => {
this.setState({ jwt: "" });
};
render() {
let loginLink;
if (this.state.jwt === "") {
loginLink = <Link to="/login">Login</Link>;
} else {
loginLink = (
<Link to="/logout" onClick={this.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"></hr>
</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>
{this.state.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 Catalogue</Link>
</li>
</Fragment>
)}
</ul>
<pre>
{JSON.stringify(this.state, null, 3)}
</pre>
</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) => <Login {...props} handleJWTChange={this.handleJWTChange} />} />
<Route exact path="/genres">
<Genres />
</Route>
<Route path="/admin/movie/:id" component={(props) => (
<EditMovie {...props} jwt={this.state.jwt} />
)}
/>
<Route path="/admin">
<Admin />
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</div>
</div>
</div>
</Router>
);
}
}
ไฟล์ EditMovie.js แก้ไขโค้ดดังนี้
import React, { Component, Fragment } from "react";
import { Link } from "react-router-dom";
import "./EditMovie.css";
import Input from "./form-components/Input";
import Select from "./form-components/Select";
import TextArea from "./form-components/TextArea";
import Alert from "./ui-components/Alert";
import { confirmAlert } from "react-confirm-alert";
import "react-confirm-alert/src/react-confirm-alert.css";
export default class EditMovie extends Component {
constructor(props) {
super(props);
this.state = {
movie: {
id: 0,
title: "",
release_date: "",
runtime: "",
mpaa_rating: "",
rating: "",
description: "",
},
mpaaOptions: [
{ id: "G", value: "G" },
{ id: "PG", value: "PG" },
{ id: "PG13", value: "PG13" },
{ id: "R", value: "R" },
{ id: "NC17", value: "NC17" },
],
isLoaded: false,
error: null,
errors: [],
alert: {
type: "d-none",
message: "",
},
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
componentDidMount() {
if (this.props.jwt === "") {
this.props.history.push({
pathname: "/login",
});
return;
}
const id = this.props.match.params.id;
if (id > 0) {
fetch("http://localhost:4000/v1/movie/" + id)
.then((response) => {
if (response.status !== "200") {
let err = Error;
err.message = "Invalid response code: " + response.status;
this.setState({ error: err });
}
return response.json();
})
.then((json) => {
const releaseDate = new Date(json.movie.release_date);
this.setState(
{
movie: {
id: id,
title: json.movie.title,
release_date: releaseDate.toISOString().split("T")[0],
runtime: json.movie.runtime,
mpaa_rating: json.movie.mpaa_rating,
rating: json.movie.rating,
description: json.movie.description,
},
isLoaded: true,
},
(error) => {
this.setState({
isLoaded: true,
error,
});
}
);
});
} else {
this.setState({ isLoaded: true });
}
}
handleSubmit = (evt) => {
evt.preventDefault();
// do validation
let errors = [];
if (this.state.movie.title === "") {
errors.push("title");
}
this.setState({ errors: errors });
if (errors.length > 0) {
return false;
}
// we passed, so post info
const data = new FormData(evt.target);
const payload = Object.fromEntries(data.entries());
const myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
myHeaders.append("Authorization", "Bearer " + this.props.jwt);
const requestOptions = {
method: "POST",
body: JSON.stringify(payload),
headers: myHeaders,
};
fetch("http://localhost:4000/v1/admin/editmovie", requestOptions)
.then((response) => response.json())
.then((data) => {
if (data.error) {
this.setState({
alert: { type: "alert-danger", message: data.error.message },
});
} else {
this.props.history.push({
pathname: "/admin",
});
}
});
};
handleChange = (evt) => {
let value = evt.target.value;
let name = evt.target.name;
this.setState((prevState) => ({
movie: {
...prevState.movie,
[name]: value,
},
}));
};
// *** add this
hasError(key) {
return this.state.errors.indexOf(key) !== -1;
}
confirmDelete = (e) => {
console.log("would delete id", this.state.movie.id);
confirmAlert({
title: "Delete Movie?",
message: "Are you sure?",
buttons: [
{
label: "Yes",
onClick: () => {
// delete the movie
fetch(
"http://localhost:4000/v1/admin/deletemovie/" +
this.state.movie.id,
{ method: "GET" }
)
.then((response) => response.json)
.then((data) => {
if (data.error) {
this.setState({
alert: {
type: "alert-danger",
message: data.error.message,
},
});
} else {
this.setState({
alert: { type: "alert-success", message: "Movie deleted!" },
});
this.props.history.push({
pathname: "/admin",
});
}
});
},
},
{
label: "No",
onClick: () => {},
},
],
});
};
render() {
let { movie, isLoaded, error } = this.state;
if (error) {
return <div>Error: {error.message}</div>;
} else if (!isLoaded) {
return <p>Loading...</p>;
} else {
return (
<Fragment>
<h2>Add/Edit Movie</h2>
<Alert
alertType={this.state.alert.type}
alertMessage={this.state.alert.message}
/>
<hr />
<form onSubmit={this.handleSubmit}>
<input
type="hidden"
name="id"
id="id"
value={movie.id}
onChange={this.handleChange}
/>
<Input
title={"Title"}
className={this.hasError("title") ? "is-invalid" : ""}
type={"text"}
name={"title"}
value={movie.title}
handleChange={this.handleChange}
errorDiv={this.hasError("title") ? "text-danger" : "d-none"}
errorMsg={"Please enter a title"}
/>
<Input
title={"Release Date"}
type={"date"}
name={"release_date"}
value={movie.release_date}
handleChange={this.handleChange}
/>
<Input
title={"Runtime"}
type={"text"}
name={"runtime"}
value={movie.runtime}
handleChange={this.handleChange}
/>
<Select
title={"MPAA Rating"}
name={"mpaa_rating"}
options={this.state.mpaaOptions}
value={movie.mpaa_rating}
handleChange={this.handleChange}
placeholder="Choose..."
/>
<Input
title={"Rating"}
type={"text"}
name={"rating"}
value={movie.rating}
handleChange={this.handleChange}
/>
<TextArea
title={"Description"}
name={"description"}
value={movie.description}
rows={"3"}
handleChange={this.handleChange}
/>
<hr />
<button className="btn btn-primary">Save</button>
<Link to="/admin" className="btn btn-warning ms-1">
Cancel
</Link>
{movie.id > 0 && (
<a
href="#!"
onClick={() => this.confirmDelete()}
className="btn btn-danger ms-1"
>
Delete
</a>
)}
</form>
</Fragment>
);
}
}
}
ไฟล์ Login.js แก้ไขโค้ดดังนี้
import React, { Component, Fragment } from "react";
import Input from "./form-components/Input";
import Alert from "./ui-components/Alert";
export default class Login extends Component {
constructor(props) {
super(props);
this.state = {
email: "",
password: "",
error: null,
errors: [],
alert: {
type: "d-none",
message: "",
},
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange = (evt) => {
let value = evt.target.value;
let name = evt.target.name;
this.setState((prevState) => ({
...prevState,
[name]: value,
}));
};
handleSubmit = (evt) => {
evt.preventDefault();
let errors = [];
if (this.state.email === "") {
errors.push("email");
}
if (this.state.password === "") {
errors.push("password");
}
this.setState({errors: errors});
if (errors.length > 0) {
return false;
}
const data = new FormData(evt.target);
const payload = Object.fromEntries(data.entries());
const requestOptions = {
method: "POST",
body: JSON.stringify(payload),
}
fetch("http://localhost:4000/v1/signin", requestOptions)
.then((response) => response.json())
.then((data) => {
if (data.error) {
this.setState({
alert: {
type: "alert-danger",
message: data.error.message,
}
})
} else {
console.log(data);
this.handleJWTChange(Object.values(data)[0]);
this.props.history.push({
pathname: "/admin",
})
}
})
};
handleJWTChange(jwt) {
this.props.handleJWTChange(jwt);
}
hasError(key) {
return this.state.errors.indexOf(key) !== -1;
}
render() {
return (
<Fragment>
<h2>Login</h2>
<hr />
<Alert
alertType={this.state.alert.type}
alertMessage={this.state.alert.message}
/>
<form className="pt-3" onSubmit={this.handleSubmit}>
<Input
title={"Email"}
type={"email"}
name={"email"}
handleChange={this.handleChange}
className={this.hasError("email") ? "is-invalid" : ""}
errorDiv={this.hasError("email") ? "text-danger" : "d-none"}
errorMsg={"Please enter a valid email address"}
/>
<Input
title={"Password"}
type={"password"}
name={"password"}
handleChange={this.handleChange}
className={this.hasError("password") ? "is-invalid" : ""}
errorDiv={this.hasError("password") ? "text-danger" : "d-none"}
errorMsg={"Please enter a password"}
/>
<hr />
<button className="btn btn-primary">Login</button>
</form>
</Fragment>
);
}
}
ใส่ Email : me@here.com และ Password คือ password เพื่อเข้าสู่ระบบ
จะสามารถ Login ได้ และ Logout เพื่อยกเลิกการเข้าสู่ระบบได้
credit : https://www.udemy.com/course/working-with-react-and-go-golang/