From 181f62f944ef6a1641238a8001e64c9036fadf96 Mon Sep 17 00:00:00 2001 From: kudotk Date: Mon, 24 Dec 2018 12:13:02 +0900 Subject: [PATCH] initial commit --- .gitignore | 2 + README.md | 49 +++++++++++ config/default.json | 8 ++ package.json | 25 ++++++ server.js | 176 ++++++++++++++++++++++++++++++++++++++++ views/_layout.pug | 18 ++++ views/auth-callback.pug | 8 ++ views/index.pug | 27 ++++++ views/result.pug | 7 ++ 9 files changed, 320 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/default.json create mode 100644 package.json create mode 100644 server.js create mode 100644 views/_layout.pug create mode 100644 views/auth-callback.pug create mode 100644 views/index.pug create mode 100644 views/result.pug diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39b56ab --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +config/development.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..aa6979e --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Zaim Rest API Sample + +## Overview + +ZaimのREST API実行サンプルです + +## Requirement + +* node v10.13.0 +* npm 6.4.1 + +## Install + +### 1. Zaim認証ID取得 + +https://dev.zaim.net/users/login にログイン +[新しいアプリケーションを追加]で適当に入力 +コンシューマID、コンシューマシークレットをconfig/default.jsonに追記 + +### 2. ユーザートークン取得 + +``` +$ npm install +$ npm run start +``` +ブラウザで http://127.0.0.1 を開く +OAuthの[認証実行]をクリック +Zaimの認証画面に移動するので許可する +完了画面に移動するがリダイレクトされないので、Chrome開発者ツールなどでソースを表示 +div.callbackのURL`http://127.0.0.1:4000/auth/callback?〜`をブラウザで開く +表示されたtoken、token secretをconfig/default.jsonに追記 + +## Usage + +Install後、`npm run start`で再起動 +ブラウザで http://127.0.0.1 を開き、ユーザー情報の取得、合計金額の取得を実行すると、Zaimに入力しているデータが表示される + +## Reference + +* [RubyでZaim APIを利用する](https://qiita.com/seteen/items/12f535228e2a3453764b) +* [GASからZaim APIを利用する](https://qiita.com/shutosg/items/6845057432bca551024b) + +## Licence + +[MIT](https://github.com/tcnksm/tool/blob/master/LICENCE) + +---- + +(c) 2018 kudotk \ No newline at end of file diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..610fc7b --- /dev/null +++ b/config/default.json @@ -0,0 +1,8 @@ +{ + "zaim": { + "consumerId": "", + "consumerSecret": "", + "userToken": "", + "userTokenSecret": "" + } +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3237324 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "zaim-rest-api-sample", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "NODE_ENV=development node server.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "MIT", + "dependencies": { + "body-parser": "^1.18.3", + "config": "^3.0.1", + "cors": "^2.8.5", + "express": "^4.16.4", + "express-session": "^1.15.6", + "morgan": "^1.9.1", + "oauth": "^0.9.15", + "passport": "^0.4.0", + "passport-oauth1": "^1.1.0", + "pug": "^2.0.3" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..e8bd5bc --- /dev/null +++ b/server.js @@ -0,0 +1,176 @@ +const express = require('express') +const cors = require('cors') +const passport = require('passport') +const OAuth1Strategy = require('passport-oauth1') +const session = require('express-session') +const morgan = require('morgan') +const oauth = require('oauth').OAuth +const config = require('config') +const bodyParser = require('body-parser'); + +/** + * Environment + */ + +// zaim OAuthパラメータ +const zaimOauthOptions = { + requestTokenURL: 'https://api.zaim.net/v2/auth/request', + accessTokenURL: 'https://api.zaim.net/v2/auth/access', + consumerKey: config.get('zaim.consumerId'), + consumerSecret: config.get('zaim.consumerSecret'), + signatureMethod: "HMAC-SHA1", +} + +// zaim他パラメータ +const zaim = { + oauth1StrategyOptions: Object.assign(zaimOauthOptions, + { + userAuthorizationURL: 'https://auth.zaim.net/users/auth', + callbackURL: "http://127.0.0.1:4000/auth/callback", + }), + + // Zaim user oauth token which passport sets + userToken: config.get('zaim.userToken'), + userTokenSecret: config.get('zaim.userTokenSecret') +} + + +/** + * Express + */ +const app = express() +app.use(cors()) +app.use(morgan('short')) +app.use(bodyParser.urlencoded({extended: true})) +app.set('view engine', 'pug') + + +/** + * Passport + */ +passport.serializeUser((user, done) => done(null, user)) +passport.deserializeUser((user, done) => done(null, user)) +passport.use(new OAuth1Strategy(zaimOauthOptions, + function (token, tokenSecret, profile, done) { + zaim.userToken = token + zaim.userTokenSecret = tokenSecret + return done(null, profile) + } +)); +app.use(passport.initialize()) +app.use(passport.session()) + + +/** + * Session + */ +const sessionOptions = { + secret: 'keyboard cat', + cookie: {}, + resave: false, + saveUninitialized: true +} +if (app.get('env') === 'production') { + app.set('trust proxy', 1) // trust first proxy + sessionOptions.cookie.secure = true // serve secure cookies +} +app.use(session(sessionOptions)) + + +/** + * Routing + */ + +// index +app.get('/', (req, res) => { + res.render('index', {}) +}); + +// 認証 +app.get('/auth', passport.authenticate('oauth')); + +app.get('/auth/callback', + passport.authenticate('oauth', {failureRedirect: '/'}), + function (req, res) { + res.render('auth-callback', { + token: zaim.userToken, + tokenSecret: zaim.userTokenSecret + }) + }); + + +// zaim OAuth認証情報ヘッダ +const oauthHeader = new oauth( + zaimOauthOptions.requestTokenURL, + zaimOauthOptions.accessTokenURL, + zaimOauthOptions.consumerKey, + zaimOauthOptions.consumerSecret, + '1.0A', + null, + zaimOauthOptions.signatureMethod +); + +// ユーザー情報取得 +app.get('/user/info', (req, res) => { + oauthHeader.get( + 'https://api.zaim.net/v2/home/user/verify', + zaim.userToken, + zaim.userTokenSecret, + function (e, data, zaimRes) { + if (e) console.error(e); + res.render('result', { + result: JSON.stringify(JSON.parse(data), null, ' ') + }) + }); +}) + +// 指定カテゴリの合計金額取得 +app.post('/category/total', (req, res) => { + const category = req.body.category + + oauthHeader.get( + 'https://api.zaim.net/v2/home/category', + zaim.userToken, + zaim.userTokenSecret, + function (e, data, zaimRes) { + if (e) { + console.log(e) + return + } + + const json = JSON.parse(data) + let categoryId + json.categories.some((elm) => { + if (elm.name === category) { + categoryId = elm.id + return true + } + return false + }) + if (!categoryId) { + res.render('result', { + result: "カテゴリが見つかりませんでした" + }) + return + } + + let total = 0 + const params = '?mapping=1&category_id=' + categoryId + '&mode=payment' + oauthHeader.get( + 'https://api.zaim.net/v2/home/money' + params, + zaim.userToken, + zaim.userTokenSecret, + function (e, data, zaimRes) { + const json = JSON.parse(data) + json.money.forEach((elm) => { + total += elm.amount + }) + res.render('result', { + result: JSON.stringify(JSON.parse('{ "totalAmount": ' + total + '}'), null, ' ') + }) + }) + + }); +}) + +app.listen(4000) diff --git a/views/_layout.pug b/views/_layout.pug new file mode 100644 index 0000000..1e49b2a --- /dev/null +++ b/views/_layout.pug @@ -0,0 +1,18 @@ +html(lang="en") + head + + meta(charset="utf-8") + meta(name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no") + title + | Zaim Rest API Sample + link(rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous") + body + div.container.p-0.m-4 + h1.font-weight-light Zaim Rest API Sample + block content + + script(src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous") + script(src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.6/umd/popper.min.js" integrity="sha384-wHAiFfRlMFy6i5SRaxvfOCifBUQy1xHdJ/yoi7FRNXMRBu5WHdZYu1hA6ZOblgut" crossorigin="anonymous") + script(src="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/js/bootstrap.min.js" integrity="sha384-B0UglyR+jN6CkvvICOB2joaf5I4l3gm9GU6Hc1og6Ls7i6U/mkkaduKaBhlAXv9k" crossorigin="anonymous") + + \ No newline at end of file diff --git a/views/auth-callback.pug b/views/auth-callback.pug new file mode 100644 index 0000000..4933522 --- /dev/null +++ b/views/auth-callback.pug @@ -0,0 +1,8 @@ +extends _layout + +block content + div.container.mt-5 + h2.font-weight-light.mt-4 token + span= token + h2.font-weight-light.mt-4 token secret + span= tokenSecret diff --git a/views/index.pug b/views/index.pug new file mode 100644 index 0000000..61dc00f --- /dev/null +++ b/views/index.pug @@ -0,0 +1,27 @@ +extends _layout + +block content + div.container.mt-5.mr-0.mb-0.ml-0 + h2.font-weight-light OAuth + a.mt-2.btn.btn-primary(href="/auth" role="button") 認証実行 + div.row.mt-2.ml-0.mr-0.mb-0.p-0 + small.text-muted.w-100 + | [認証が完了]画面でリダイレクトされない場合、 + br + | Chromeの開発者ツールなどでソースを開き、 + span.text-success.font-weight-bold div.callbackのURL + | をブラウザで開いてOAuthトークンを取得してください。 + div.container.mt-5.mr-0.mb-0.ml-0 + div.container.p-0.m-0 + h2.font-weight-light ユーザー情報の取得 + a.btn.btn-primary.mt-2(href="/user/info" role="button") 実行 + div.container.mt-5.mr-0.mb-0.ml-0 + div.container.p-0.m-0 + h2.font-weight-light 合計金額の取得 + form(action="/category/total" method="POST") + .form-group + label(for="category") + input.form-control.w-25(type="text" name="category" placeholder="カテゴリを入力" aria-describedby="categoryHelp") + small#categoryHelp.form-text.text-muted + | 合計金額を取得したいカテゴリを入力 + button.btn.btn-primary.mt-2(type="submit") 実行 diff --git a/views/result.pug b/views/result.pug new file mode 100644 index 0000000..57abcf8 --- /dev/null +++ b/views/result.pug @@ -0,0 +1,7 @@ +extends _layout + +block content + div.container.mt-5 + h2.font-weight-light.mt-4 レスポンス + pre + code=result