Posted on

Golangを使ってJWTを15分で理解する

JWTとは

JWT(ジョットと言うらしい)はJSON Web Tokenの略で、JSONをベースとしたアクセストークンのためのオープン標準 (RFC 7519) です。色々記事を見ましたが、最終的にWikipediaが分かりやすく一番参考にしました。
https://ja.wikipedia.org/wiki/JSON_Web_Token

JWTの構造

JWTは以下の3つの要素をピリオドで区切った文字列で構成されます。

ヘッダー

署名生成に使用するアルゴリズムを格納します。下記のHS256は、このトークンがHMAC-SHA256で署名されていることを示しています。署名アルゴリズムとしては、SHA-256を使用したHMAC (HS256) や、SHA-256を使用したRSA署名 (RS256) がよく用いられます。

{
 "alg" : "HS256",
 "typ" : "JWT"
}

ペイロード

認証情報などのクレームを格納します。クレームとはペイロードに含める以下のような標準フィールド(クレーム)を指します。JWTの仕様では、トークンに一般的に含まれる7つの標準フィールドが定義されています。また用途に応じた独自のカスタムフィールドを含むこともできます。下記の例では、トークン発行日時を示す標準のクレーム (iat) と、カスタムクレーム (loggedInAs) を格納しています。

{
 "loggedInAs" : "admin",
 "iat" : 1422779638
}
7つのペイロードの標準クレーム

署名

トークン検証用の署名です。署名はヘッダーとペイロードをBase64urlエンコーディングしてピリオドで結合したものから生成します。署名はヘッダーで指定された暗号化アルゴリズムにより生成されます。下記はHMAC-SHA256形式でのコード例です。

HMAC-SHA256(base64urlEncoding(header) + '.' + base64urlEncoding(payload), 'secret key')

JWTを使用するにあたって

JWTはトークンが返され、それをローカルに保存して利用します(主にlocal storageやsession storageが用いられますが、セッションIDのようにCookieを用いる場合もあります。) 認証時にはAuthorizationヘッダーでBearerスキーマを利用します。またサーバー上に認証状態を保持しないステートレスな認証方式です。その為JWT単体ではトークンを無効にすることが出来ません。サーバーに状態を保持すれば可能ですが、その場合ステートレスの利点は失われることになります。
さて、ここまではほぼ Wikipedia に書いてある内容そのままです。ここから実際にGo/GinJWT Middlewareを使って実際の動作を確認してみます。

Go/GinJWT Middlewareを使った動作確認

利用するJWT Middlewareについて

ここでは、「https://github.com/gin-gonic/gin」 を使う前提で、次のMiddlewareを利用します。「https://github.com/appleboy/gin-jwt」。このMiddlewareは、auth_jwt.goの1ファイルでで構成されていて、「https://github.com/dgrijalva/jwt-go」Gin用に薄くラップしたものです。jwt-goはトークンを作成したりパースしたり様々な機能が用意されています。

サンプルソース

サンプルソースは、https://github.com/appleboy/gin-jwt/blob/master/README.md に載っているのでこれを元に確認します。処理は大きく「ログイン時にToken発行する」と「トークン認証&処理実行する」の2種類あります。

ログイン時にToken発行する

ログイン時にTokenを発行する処理は、LoginHandlerです。Routerでは次のように定義しています。LoginHandlerではAuthenticatorPayloadFuncが呼ばれる為、Middlewareにてこれらを実装する必要があります。

r.POST("/login", authMiddleware.LoginHandler)

Authenticatorはログイン認証の為の関数です。例では固定値が設定されていますが、実際は主にDBから値を取得することになると思います。PayloadFuncはペイロードに含めるクレームを設定します。ペイロードには任意のクレームを追加可能なので、ログインIDとなるuserIDをセットしています。

// ログインに基づいたユーザの認証振る舞いをするコールバック
Authenticator: func(c *gin.Context) (interface{}, error) {
  var loginVals login
  if err := c.ShouldBind(&loginVals); err != nil {
    return "", jwt.ErrMissingLoginValues
  }
  userID := loginVals.Username
  password := loginVals.Password
  // ユーザIDとパスワード認証(実際は主にDBから値を取得する)
  if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
    return &User{
      UserName:  userID,
    }, nil
  }
  return nil, jwt.ErrFailedAuthentication
},
// ペイロードのクレーム設定
PayloadFunc: func(data interface{}) jwt.MapClaims {
  if v, ok := data.(*User); ok {
    return jwt.MapClaims{
      IdentityKey: v.UserName,
    }
  }
  return jwt.MapClaims{}
}

ログイン時にアクセストークンを取得するにはサーバ起動後に以下のコマンドを実行します。

% http -v --json POST localhost:8000/login username=admin password=admin

実行すると次のような結果が得られます。

POST login HTTP/1.1
Accept: application/json, */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 42
Content-Type: application/json
Host: localhost:8000
User-Agent: HTTPie/1.0.2
{
    "password": "admin",
    "username": "admin"
}
HTTP/1.1 200 OK
Content-Length: 213
Content-Type: application/json; charset=utf-8
Date: Tue, 27 Aug 2019 03:26:18 GMT
{
    "code": 200,
    "expire": "2019-08-27T13:26:18+09:00",
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjY4Nzk5NzgsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTU2Njg3NjM3OH0.D0z_y8vLEeQW_mtgOCw6gfrmz6eSGfW6uOG7KoEaMAo"
}

LoginHandlerの処理を見てみる

ここであらためてauth_jwt.goLoginHandlerの処理を見てみると内部で何をやっているかが具体的に分かります。エラーチェックなどを省いて主要なところだけ確認してみます。
上記middlewareで実装しているAuthenticatorを呼んでログイン認証します。

data, err := mw.Authenticator(c)

ログイン認証が通ったらクレームを取得します。

// Create the token
token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
claims := token.Claims.(jwt.MapClaims)

上記で実装しているPayloadFuncを呼んで独自クレームを設定します。

if mw.PayloadFunc != nil {
  for key, value := range mw.PayloadFunc(data) {
    claims[key] = value
  }
}

更にペイロードに必要なクレーム情報を設定します。expはトークン切れタイムで、orig_iatはトークン生成タイムです。

expire := mw.TimeFunc().Add(mw.Timeout)
claims["exp"] = expire.Unix()
claims["orig_iat"] = mw.TimeFunc().Unix()

最後に署名付きトークンを生成してレスポンスとして返します。

tokenString, err := mw.signedString(token)
mw.LoginResponse(c, http.StatusOK, tokenString, expire)

これらを組み合わせたLoginHandlerの実際の処理です。

func (mw *GinJWTMiddleware) LoginHandler(c *gin.Context) {
    if mw.Authenticator == nil {
        mw.unauthorized(c, http.StatusInternalServerError, mw.HTTPStatusMessageFunc(ErrMissingAuthenticatorFunc, c))
        return
    }
    data, err := mw.Authenticator(c)
    if err != nil {
        mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
        return
    }
    // Create the token
    token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
    claims := token.Claims.(jwt.MapClaims)
    if mw.PayloadFunc != nil {
        for key, value := range mw.PayloadFunc(data) {
            claims[key] = value
        }
    }
    expire := mw.TimeFunc().Add(mw.Timeout)
    claims["exp"] = expire.Unix()
    claims["orig_iat"] = mw.TimeFunc().Unix()
    tokenString, err := mw.signedString(token)
    if err != nil {
        mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrFailedTokenCreation, c))
        return
    }
    // set cookie
    if mw.SendCookie {
        maxage := int(expire.Unix() - time.Now().Unix())
        c.SetCookie(
            mw.CookieName,
            tokenString,
            maxage,
            "/",
            mw.CookieDomain,
            mw.SecureCookie,
            mw.CookieHTTPOnly,
        )
    }
    mw.LoginResponse(c, http.StatusOK, tokenString, expire)
}
func (mw *GinJWTMiddleware) signedString(token *jwt.Token) (string, error) {
    var tokenString string
    var err error
    if mw.usingPublicKeyAlgo() {
        tokenString, err = token.SignedString(mw.privKey)
    } else {
        tokenString, err = token.SignedString(mw.Key)
    }
    return tokenString, err
}

払い出されたTokenを検証してみる

払い出されたTokenを検証してみます。トークンはピリオドで分割し3つに分けることができます。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjY4Nzk5NzgsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTU2Njg3NjM3OH0.D0z_y8vLEeQW_mtgOCw6gfrmz6eSGfW6uOG7KoEaMAo
  • ヘッダー:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • ペイロード:eyJleHAiOjE1NjY4Nzk5NzgsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTU2Njg3NjM3OH0
  • 署名:D0z_y8vLEeQW_mtgOCw6gfrmz6eSGfW6uOG7KoEaMAo

ヘッダーとペイロードはbase64でエンコードされているだけなので以下のコマンドで確認することができます。
・ヘッダー

% echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -D
{"alg":"HS256","typ":"JWT"}%

・ペイロード

% echo eyJleHAiOjE1NjY4Nzk5NzgsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTU2Njg3NjM3OH0 | base64 -D
{"exp":1566879978,"id":"admin","orig_iat":156687637%

・署名は暗号化されているので当然復元できません。

% echo D0z_y8vLEeQW_mtgOCw6gfrmz6eSGfW6uOG7KoEaMAo | base64 -D
L������k`8,:���ϧ���*�%

またトークンを確認するのに便利なサイトがあります。このサイトを利用するとJWT認証の確認が簡単にできます。認証ができていないと、Invalid Signatureとなりますが、シークレットキーに今回使った文字列secret keyと入力し今回のトークンを貼り付けるとSignature Verifiedと認証成功が確認できます。
https://jwt.io/
・認証NG

・認証OK

トークン認証&処理実行する

次にトークン認証と認証後の処理の実行をみてみます。auth_jwt.goではMiddlewareFuncを呼び出します。RouterにはauthMiddleware.MiddlewareFunc()をかまし、その中に実行したいRouteを設定します。

auth := r.Group("/auth")
auth.Use(authMiddleware.MiddlewareFunc())
{
    auth.GET("/hello", helloHandler)
}
func helloHandler(c *gin.Context) {
    claims := jwt.ExtractClaims(c)
    user, _ := c.Get(middleware.IdentityKey)
    c.JSON(200, gin.H{
        "userID":   claims["id"],
        "userName": user.(*middleware.User).UserName,
        "text":     "Hello World.",
    })
}

middleware側では、IdentityHandlerAuthorizatorをあらかじめ実装しておく必要があります。

// クレームからログインIDを取得する
IdentityHandler: func(c *gin.Context) interface{} {
  claims := jwt.ExtractClaims(c)
  return &User{
    UserName: claims["id"].(string),
  }
},
// トークンのユーザ情報からの認証
Authorizator: func(data interface{}, c *gin.Context) bool {
  // UserNameは主にDBから取得
  if v, ok := data.(*User); ok && v.UserName == "admin" {
    return true
  }
  return false
},

トークンを使ってリクエストを投げるとリクエストが実行されます。

http -f GET localhost:8000/auth/hello "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjY4Nzk5NzgsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTU2Njg3NjM3OH0.D0z_y8vLEeQW_mtgOCw6gfrmz6eSGfW6uOG7KoEaMAo"  "Content-Type: application/json"

正常にレスポンスが取得できました。

http -f GET localhost:8000/auth/hello "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjY4ODQ5ODQsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTU2Njg4MTM4NH0.b22SkPtS5q5-YRHf9GCsUstvcKsNh2ds1fRdZN-Yxac"  "Content-Type: application/json"
HTTP/1.1 200 OK
Content-Length: 60
Content-Type: application/json; charset=utf-8
Date: Tue, 27 Aug 2019 04:50:04 GMT
{
    "text": "Hello World.",
    "userID": "admin",
    "userName": "admin"
}

試しに署名の最後の4文字をaaaaに変更してみると署名の認証エラーであるsignature is invalidが返ってきます。

http -f GET localhost:8000/auth/hello "Authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjY4ODQ5ODQsImlkIjoiYWRtaW4iLCJvcmlnX2lhdCI6MTU2Njg4MTM4NH0.b22SkPtS5q5-YRHf9GCsUstvcKsNh2ds1fRdZN-aaaa"  "Content-Type: application/json"
HTTP/1.1 401 Unauthorized
Content-Length: 46
Content-Type: application/json; charset=utf-8
Date: Tue, 27 Aug 2019 04:50:59 GMT
Www-Authenticate: JWT realm=test zone
{
    "code": 401,
    "message": "signature is invalid"
}

MiddlewareFuncの処理を見てみる

では実際中ではどのような処理が行われているかauth_jwt.goMiddlewareFuncの主要処理をみてみます。
トークンからクレームを取得します。

claims, err := mw.GetClaimsFromJWT(c)

クレーム内のexpをチェックしトークンが有効かをチェックします。

if claims["exp"] == nil {
  mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
  return
}
if _, ok := claims["exp"].(float64); !ok {
  mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c))
  return
}
if int64(claims["exp"].(float64)) < mw.TimeFunc().Unix() {
  mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
  return
}

ミドルウェアで実装しているIdentityHandlerを呼び出し、クレームからユーザ情報を取得します。

IdentityHandler: func(c *gin.Context) interface{} {
  claims := jwt.ExtractClaims(c)
  return &User{
    UserName: claims["id"].(string),
  }
},

ミドルウェアで実装しているAuthorizatorを呼び出しIDが一致していたら認証OKとなります。

// トークンのユーザ情報からの認証
Authorizator: func(data interface{}, c *gin.Context) bool {
  // UserNameは主にDBから取得
  if v, ok := data.(*User); ok && v.UserName == "admin" {
    return true
  }
  return false
},

MiddlewareFuncの全処理です。

func (mw *GinJWTMiddleware) MiddlewareFunc() gin.HandlerFunc {
    return func(c *gin.Context) {
        mw.middlewareImpl(c)
    }
}
func (mw *GinJWTMiddleware) middlewareImpl(c *gin.Context) {
    claims, err := mw.GetClaimsFromJWT(c)
    if err != nil {
        mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
        return
    }
    if claims["exp"] == nil {
        mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
        return
    }
    if _, ok := claims["exp"].(float64); !ok {
        mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c))
        return
    }
    if int64(claims["exp"].(float64)) < mw.TimeFunc().Unix() {
        mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
        return
    }
    c.Set("JWT_PAYLOAD", claims)
    identity := mw.IdentityHandler(c)
    if identity != nil {
        c.Set(mw.IdentityKey, identity)
    }
    if !mw.Authorizator(identity, c) {
        mw.unauthorized(c, http.StatusForbidden, mw.HTTPStatusMessageFunc(ErrForbidden, c))
        return
    }
    c.Next()
}

これで一通りの動作確認ができました。

まとめ

はじめ知らないワードが出てきて理解しきれなかったのですが、実際に触ってみることでJWTについてある程度理解できました。触ってみてステートレスであり、ステートレスであるがゆえにJWT単体ではトークンを無効にすることが出来ないという理由も実感できました。

その他の参考記事

https://qiita.com/k_k_hogetaro/items/0c97f42ecb8207767db2

Posted on

コーチング入門 オススメコーチング書籍3冊!!

これまで特別意識してこなかったコーチングについて意識する機会があったのでオススメしてもらった3冊を読みました。学びがあったこととその感覚の定着の為にメモしておきます。



背景

大学を卒業してソフトウェアエンジニアになり、いちエンジニアからチームリーダー、テックリード、マネージャー、スタートアップのCTOを経験してきました。その中でチームマネージメントや1on1などもやってきました。ただこれまでやってきたことは自分の経験であったり周囲との関わりで得たものをベースに自分なりに考えてやっていました。
そして今、新しい環境(スタートアップ)にジョインしたところ、今の環境には1on1(コーチング)を積極的に進めているエンジニアがいました。そしてそれはこれまで半ばやらされている感じでやっていた1on1とは印象が違いました。そこでもれなく自分もコーチングをしてもらうようになったわけですが、そのコミュニケーションの中でこれまでの自分の活動を振り返ってみると自分が今「コーチング」に興味があることが分かりました(年齢的なこともあると思います)。「マネージメント」と聞くとちょっと抵抗感があるけど「コーチング」と聞くと抵抗感がないという不思議さもあります。そんなこんなでオススメしてもらった書籍を読み、体系的に学びを得た上で、今後の活動に活かそうと思います。

1冊目 コーチング・マネジメント―人と組織のハイパフォーマンスをつくる

書評

2002年に出版された本なので今から約20年程前の本ですが、読んでいても時間の経過による内容のずれなどは全く気になりません。前半から後半にかけては、コーチングの基本・詳細と進んでいき最後の方にはコーチングを導入するにあたってのチェックリストがあるといった構成になっています。中でも一番の要点は、「コーチング・フロー」と「いかに聞くのか?」ということだと思います。「コーチング・フロー」は次のとおりです。

  1. 現状の明確化
  2. 望ましい状態の明確化
  3. ギャップを引き起こしている理由と背景の発見
  4. 行動計画を立てる
  5. フォロー

これだけ見ると当たり前のようですが、これを実際に実践する・させる為に「いかに聞くのか?」というスキルが求められます。本の中ではそのコミュニケーションへのアプローチの方法やコミュニケーションが如何に大事かが書かれています。やり方や捉え方によっては詰めているようにも感じられそうな程の質問攻めですがそこのバランスが重要なポイントになるかと思います。

2冊目 この1冊ですべてわかる 新版 コーチングの基本

書評

2009年に出版された書籍の新版(2019年)です。この本は「コーチング・マネジメント」より詳細に具体的にコーチングについて書かれています。後半では実例による説明もあります。目次がパッと見分かりやすかったので記載しておきます。

  • 1章 コーチングとは何か
  • 2章 コーチのもつべき視点
  • 3章 コーチングの3原則
  • 4章 コーチング・プロセス
  • 5章 コーチングのスキルと実践例
  • 6章 組織へのコーチング

本書でも4章で「コーチング・プロセス」とし「コーチング・マネジメント」の「コーチング・フロー」が書かれています。本書で特筆すべきは「コーチが持つべき3つの視点」として以下の3つ(PBPの視点)について言及されていることです。これらは三角形となりそれぞれ相互に作用しているようです。

  • Possesion(身につけるもの)
  • Behavior(行動)
  • Presence(考え方、信念)

全体的に図が多く具体的に書かれているので「コーチング・マネジメント」と合わせて読むとより理解が深まると思います。

3冊目 0秒リーダーシップ:「これからの世界」で圧倒的な成果を上げる仕事術

書評

2016年に出版された本で、上記2冊と違い著者が外国人です。著者は日本に長年いながらも、グーグルやモルガン・スタンレーで人材開発を務めていたとあって外国から見た日本という視点がおもしろい点です。上記2冊と違いコーチングではなく、リーダーシップはこうあるべきということが書かれています。グーグルの話や、マインドフルネス、禅などの話も出てきます。コーチングという文脈ではないので上記2冊+αな気持ちで読むとよいかもしれません。
本書で気になったワード

僕はよく英語で、「Leadership is mobilzing people to tackle tough problems.(リーダーシップとは、難問に取り組むために人々を動かしていくこと)」という定義を用います。
Learn, Relearn, Unlearn
学ぶことは大事だが、ただ知識を増やす(Learn)だけではなく、学び直す(relearn)の必要があります。完全に時代遅れになった考え方、価値観や信念は手放す(unlearn)べきです。
Posted on

Vue.js+TypeScriptで外部APIを使ったTODOリストを作ってみた

Vue.jsで外部APIを使ったTODOリストを作ってみた に続き、それのTypeScript版を作ってみました。TODOリスト用のAPIは以前書いたこちらのAPI「Go言語 GORM+GinでTODOリストのAPIを作ってみた」を利用します。CORSを全て許可しているのでどこからでも叩けるようになっています。TypeScriptを書くのは今回が初めてなので、誤っている箇所やもっとよい書き方などがあれば指摘して頂ければと思います。

できたもの

できたものはこちらです。
http://vuejs-ts.taisablog.com/todo

APIのエンドポイント

APIのエンドポイントは以下としました。

URL    http://gin.taisablog.com/api/v1/
GET    /todo       // 一覧表示
POST   /todo       // 新規作成
GET    /todo/:id   // 編集画面表示
PUT    /todo/:id   // 編集(今回未使用)
DELETE /todo/:id   // 削除

プロジェクト作成

vue-cliを使ってプロジェクト作成をしました。プロジェクト作成のコマンドを打つと、色々と聞かれますが、TypeScriptを利用する為にManually select featuresを選択し、TypeScriptをONにします。ここではRouterもONにしました。

% npm install -g @vue/cli
% vue create my-project
  default (babel, eslint)
❯ Manually select features

vue-cliでできたプロジェクトのsrc配下の構成は以下となっています。今回はそこにTodo.vueTodoList.vueを追加して実装しました。views配下で実装するだけでも大丈夫ですが、今回はあえてviews/Todo.vueからTodoList.vueコンポーネントを呼び出す形としました。

.
├── App.vue
├── assets
│   └── logo.png
├── components
│   ├── HelloWorld.vue
│   └── TodoList.vue ← 新規追加
├── main.ts
├── router.ts
├── shims-tsx.d.ts
├── shims-vue.d.ts
└── views
    ├── About.vue
    ├── Home.vue
    └── Todo.vue ← 新規追加

views/Todo.vue

TodoList.vueコンポーネントを呼び出します。


  
Vue logo
import { Component, Vue } from 'vue-property-decorator' import TodoList from '@/components/TodoList.vue' @Component({ components: { TodoList, }, }) export default class Todo extends Vue {}

components/TodoList : importとクラス定義

axiosを利用するのでインストールします。

% npm i axios
import { Component, Vue } from 'vue-property-decorator'
import axios from 'axios'
const NOT_STARTED = 1
const FINISHED = 3
@Component
export default class TodoList extends Vue {
   ここに実装
}

インスタンス変数定義

private todoList: string[] = []
private inputField: string = ''
private baseUrl: string = 'http://gin.taisablog.com/api/v1/'

createdフック

createdフックでロード時に一覧を取得します。

public created() {
  this.getTodo()
}

一覧を取得する

public async getTodo() {
  try {
    const response = await axios.get(this.baseUrl + 'todo')
    this.todoList = response.data
    return this.todoList
  } catch (e) {
    return e
  }
}

タスクを追加する

public async addTodo() {
  if (!this.inputField) {
    return
  }
  try {
    const params = {
      text: this.inputField,
      status: 1,
    }
    await axios.post(this.baseUrl + 'todo', JSON.stringify(params))
    this.getTodo()
    this.inputField = ''
  } catch (e) {
    return e
  }
}

タスクを削除する

public async deleteTodo(todo: any) {
  try {
    await axios.delete(this.baseUrl + 'todo/' + todo.ID)
    this.getTodo()
  } catch (e) {
    return e
  }
}

タスクを完了にする

public async toggle(todo: any) {
  try {
    let status = 0
    if (todo.Status === NOT_STARTED) {
      status = FINISHED
    } else {
      status = NOT_STARTED
    }
    const params = {
      '{status}': status,
    }
    await axios.put(this.baseUrl + 'todo/' + todo.ID, JSON.stringify(params))
    todo.Status = status
  } catch (e) {
    return e
  }
}

HTML

HTMLとCSSはもう少し書き直したいです。


    

Vue.js TODO List

  • {{ todo.Text }}
    {{ todo.Text }}
    X

TodoList.vue全部


    

Vue.js TODO List

  • {{ todo.Text }}
    {{ todo.Text }}
    X
import { Component, Vue } from 'vue-property-decorator' import axios from 'axios' const NOT_STARTED = 1 const FINISHED = 3 @Component export default class TodoList extends Vue { private todoList: string[] = [] private inputField: string = '' private baseUrl: string = 'http://gin.taisablog.com/api/v1/' public created() { this.getTodo() } public async getTodo() { try { const response = await axios.get(this.baseUrl + 'todo') this.todoList = response.data return this.todoList } catch (e) { return e } } public async addTodo() { if (!this.inputField) { return } try { const params = { text: this.inputField, status: 1, } await axios.post(this.baseUrl + 'todo', JSON.stringify(params)) this.getTodo() this.inputField = '' } catch (e) { return e } } public async deleteTodo(todo: any) { try { await axios.delete(this.baseUrl + 'todo/' + todo.ID) this.getTodo() } catch (e) { return e } } public async toggle(todo: any) { try { let status = 0 if (todo.Status === NOT_STARTED) { status = FINISHED } else { status = NOT_STARTED } const params = { '{status}': status, } await axios.put(this.baseUrl + 'todo/' + todo.ID, JSON.stringify(params)) todo.Status = status } catch (e) { return e } } } .todoList { width: 100%; } .clearfix::after { content: ''; display: block; clear: both; } .inputWrapper { position: relative; width: 380px; margin: auto; display: block; } .inputWrapper input[type='text'] { font: 15px/24px sans-serif; box-sizing: border-box; width: 100%; padding: 0.3em; transition: 0.3s; letter-spacing: 1px; border: 1px solid #1b2538; border-radius: 4px; } .ef input[type='text']:focus { border: 1px solid #da3c41; outline: none; box-shadow: 0 0 5px 1px rgba(218, 60, 65, .5); } .txtBoxWrapper { float: left; width: 270px; } .addBtnWrapper { float: right; } .addBtn { position: relative; display: block; text-decoration: none; color: #FFF; background: #007bff; border: solid 1px #007bff; border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2); text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); width: 100px; height: 35px; font-size: 16px; } ul { list-style: none; } li { border: 1px solid #dee2e6; border-top-left-radius: .25rem; border-top-right-radius: .25rem; margin: 10px auto; width: 50%; height: 80px; } .todo { display: flex; justify-content: space-between; align-items: center; width: 100%; height: 100%; } .chkboxLabel { width: 20px; display: inline-block; text-align: left; margin-right: 10px; } .chkbox { transform: scale(1.3); margin-left: 10px; } .todoTxt { font-size: 20px; width: 100%; text-align: center; vertical-align: middle; display: inline-block; } .todoTxt.NotStarted { text-decoration: none; } .todoTxt.Finished { text-decoration: line-through; } .deleteBtn { color: pink; text-align: right; margin-right: 20px; margin-left: 10px; width: 20px; display: block; font-size: 20px; cursor: pointer; } @media screen and (max-width: 520px) { ul { list-style: none; padding: 0; margin: 0; } li { border: 1px solid #dee2e6; border-top-left-radius: .25rem; border-top-right-radius: .25rem; margin: 10px 0; width: 100%; height: 80px; padding: 0; } .inputWrapper { margin: 0px auto } }

ソース

https://github.com/taisa831/sandbox-vuejs-ts

まとめ

TypeScriptを書くのが初めてだったので(書籍自体あまりなかったですが)事前に2冊購入して読みました。学習コストが大変かと思っていましたが、評判通りJavaやC#のようなサーバサイドのように書けるので書きやすくてよいです。


Posted on

Vue.jsで外部APIを使ったTODOリストを作ってみた

APIを使ったTODOリストをVue.jsで作ってみました。TODOリスト用のAPIは以前書いたこちらのAPI「Go言語 GORM+GinでTODOリストのAPIを作ってみた」を利用します。CORSを全て許可しているのでどこからでも叩けるようになっています。

できたもの

できたものはこちらです。http://vuejs.taisablog.com/todo

APIのエンドポイント

APIのエンドポイントは以下としました。

URL    http://gin.taisablog.com/api/v1/
GET    /todo       // 一覧表示
POST   /todo       // 新規作成
GET    /todo/:id   // 編集画面表示
PUT    /todo/:id   // 編集(今回未使用)
DELETE /todo/:id   // 削除

TODOリストの処理

プロジェクトはvue-cliで作成し、APIはaxiosを利用しました。componentsTodo.vueファイルを作成しそこにすべての処理を書いています。

インポート

import axios from 'axios'
const NOT_STARTED = 1 // 未対応
const STARTED = 2 // 対応中
const FINISHED = 3 // 完了

data function

name: "Todo",
data() {
  return {
    todoList: [],
    inputField: '',
    isActive: false,
    baseUrl: 'http://gin.taisablog.com/api/v1/'
  }
},

created function

created() {
  this.getTodo()
},

methods : 一覧を取得する

async getTodo() {
  try {
    let response = await axios.get(this.baseUrl + 'todo')
    this.todoList = response.data
  } catch (e) {
    console.log(e)
  }
},

methods : タスクを追加する

async addTodo() {
  // inputFieldの空チェック
  if (!this.inputField) {
    return
  }
  try {
    let params = {
      'text': this.inputField,
      'status': 1
    }
    await axios.post(this.baseUrl + 'todo', JSON.stringify(params))
    // 一覧取得
    this.getTodo()
    // inputFieldを空にする
    this.inputField = ''
  } catch (error) {
    console.log(error)
  }
},

methods : タスクを削除する

async deleteTodo(todo) {
  try {
    await axios.delete(this.baseUrl + 'todo/' + todo.ID)
    // 一覧を取得する
    this.getTodo()
  } catch (e) {
    console.log(e)
  }
},

methods : タスクを完了にする

async toggle(todo) {
  try {
    let status = 0
    if (todo.Status === NOT_STARTED) {
      status = FINISHED
    } else {
      status = NOT_STARTED
    }
    let params = {
      'status': status
    }
    await axios.put(this.baseUrl + 'todo/' + todo.ID, JSON.stringify(params))
    todo.Status = status
  } catch (e) {
    console.log(e)
  }
}

HTML


    

Vue.js TODO List

  • {{ todo.Text }}
    {{ todo.Text }}
    X

Todo.vue全部

HTMLとスタイルはあえてフレームワークを使わず自力で作成したのでボロボロです。


    

Vue.js TODO List

  • {{ todo.Text }}
    {{ todo.Text }}
    X
import axios from 'axios' const NOT_STARTED = 1 // 未対応 const STARTED = 2 // 対応中 const FINISHED = 3 // 完了 export default { name: "Todo", data() { return { todoList: [], inputField: '', isActive: false, baseUrl: 'http://gin.taisablog.com/api/v1/' } }, created() { this.getTodo() }, methods: { async getTodo() { try { let response = await axios.get(this.baseUrl + 'todo') this.todoList = response.data } catch (e) { console.log(e) } }, async addTodo() { if (!this.inputField) { return } try { let params = { 'text': this.inputField, 'status': 1 } await axios.post(this.baseUrl + 'todo', JSON.stringify(params)) this.getTodo() this.inputField = '' } catch (error) { console.log(error) } }, async deleteTodo(todo) { try { await axios.delete(this.baseUrl + 'todo/' + todo.ID) this.getTodo() } catch (e) { console.log(e) } }, async toggle(todo) { try { let status = 0 if (todo.Status === NOT_STARTED) { status = FINISHED } else { status = NOT_STARTED } let params = { 'status': status } await axios.put(this.baseUrl + 'todo/' + todo.ID, JSON.stringify(params)) todo.Status = status } catch (e) { console.log(e) } } }, } .todoList { width: 100%; } .clearfix::after { content: ''; display: block; clear: both; } .inputWrapper { position: relative; width: 380px; margin: auto; display: block; } .inputWrapper input[type='text'] { font: 15px/24px sans-serif; box-sizing: border-box; width: 100%; padding: 0.3em; transition: 0.3s; letter-spacing: 1px; border: 1px solid #1b2538; border-radius: 4px; } .ef input[type='text']:focus { border: 1px solid #da3c41; outline: none; box-shadow: 0 0 5px 1px rgba(218, 60, 65, .5); } .txtBoxWrapper { float: left; width: 270px; } .addBtnWrapper { float: right; } .addBtn { position: relative; display: block; text-decoration: none; color: #FFF; background: #007bff; border: solid 1px #007bff; border-radius: 4px; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2); text-shadow: 0 1px 0 rgba(0, 0, 0, 0.2); width: 100px; height: 35px; font-size: 16px; } ul { list-style: none; } li { border: 1px solid #dee2e6; border-top-left-radius: .25rem; border-top-right-radius: .25rem; margin: 10px auto; width: 50%; height: 80px; } .todo { display: flex; justify-content: space-between; align-items: center; width: 100%; height: 100%; } .chkboxLabel { width: 20px; display: inline-block; text-align: left; margin-right: 10px; } .chkbox { transform: scale(1.3); margin-left: 10px; } .todoTxt { font-size: 20px; width: 100%; text-align: center; vertical-align: middle; display: inline-block; } .todoTxt.NotStarted { text-decoration: none; } .todoTxt.Finished { text-decoration: line-through; } .deleteBtn { color: pink; text-align: right; margin-right: 20px; margin-left: 10px; width: 20px; display: block; font-size: 20px; cursor: pointer; } @media screen and (max-width: 520px) { ul { list-style: none; padding: 0; margin: 0; } li { border: 1px solid #dee2e6; border-top-left-radius: .25rem; border-top-right-radius: .25rem; margin: 10px 0; width: 100%; height: 80px; padding: 0; } .inputWrapper { margin: 0px auto } }

ソース

ソースはこちらにあがっています。
https://github.com/taisa831/sandbox-vuejs

まとめ

もっとスマートな書き方があるかしれませんが、これで一通り動作するようになりました。次はこれをTypeScriptに書き換えたものを作成する予定です。HTMLとCSSに一番時間がかかったのでもっとスマートにさらっと書けるようになりたいです。