260
243

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

社内ツールに仮想通貨を導入して、ランチ代を少し会社に負担してもらいたいという願望の話

Posted at

はじめに

社内ツールを1から作っています。まだ実装途中ですが、その話をしようと思います。

目的

  • 日報ツールや目標管理ツールが使いにくいという声の解消
  • 社内ツール放置問題の解消
  • という建前で、1から作り直し、勝手に社内仮想通貨を導入、社食のランチ代を会社側に少し負担してもらいたいという願望の実現

意識していること

  1. ユーザ(社員)が幸せになってくれるツールであること
    絶対条件です。使ってくれる人がいないと、会社がランチ代を出してくれません。
    今回、ちょうど日報ツールの社内アンケート結果があったので、それを参考にしました。
  2. エンジニアやデザイナーがワクワクするツールであること
    社内ツールは後回しにされがちですが、その理由は、本当に「運営しているサービスに手一杯だから」だけですか? 
    エンジニアがワクワクするツールにしましょう!コミットしてくれる仲間がいると信じて。

ツールの概要

日報ツール×目標管理ツールになります
今までは、日報ツールと目標管理ツールが別々に存在し、人によっては、目標を意識せずに毎日を過ごしている、、、ということがありそうだったので、こんな感じにしてみました。

日報を書くときに、目標がすぐ横に見えるので、意図せずとも目標を意識して毎日が過ごせそうですね!
という利便性を訴えつつ、メインの機能は、、、

demo.gif

日報がいいね!されると、投稿者に社内仮想通貨が支払われる機能です。(この仮想通貨を集めると、、、ランチを会社が負担してくれるようになるといいのになー、、、(願望))
いわゆる、会社が取引所のような動きをして、社内仮想通貨を日本円(今回はランチ代)に替えてくれるって感じですかね。

現在の構成

さーそろそろ、技術の話をします。

インフラはDockerでNginx、MariaDBコンテナを作り、バックエンドはRailsで実装、フロントにVue.jsとTypeScriptを使い、SPAとして動作させています。
それと、社内仮想通貨取引をBlockchainで管理しています

今回は、

  • インフラの話
  • バックエンドの話
  • フロントエンドの話
  • ブロックチェーンの話

を順に話していこうと思います

インフラの話

インフラは自動化かつ共通化されていなければなりません。
理由は2つ

  • 一人一人がバラバラの環境でやると、マージしたときに動かない可能性がある
  • インフラに詳しくない人でもすんなり入れるようにして、参加者を増やす

以下のようなディレクトリ構成になっていて、docker-compose up -dとするだけで、全てのコンテナが立ち上がり、開発準備が整います

raildock
   |- workspace
   |- nginx
   |- mariadb
   |- docker-compose.yml
   |- docker-compose.dev.yml
   |- .env.example

作るコンテナは3つで以下が概要です

コンテナ名 概要
workspace rubyやnodeなど開発に必要なツールが入ったコンテナで、開発はここの中で行います
nginx Nginxのプロセスのみが動いていて、workspaceの/tmp/sockets/puma.sockを参照しています
mariadb MariaDBのプロセスのみが動いています

人によってworkspaceに入れたい言語やパッケージ、バージョンが異なるかと思いますが、workspaceコンテナにNode.jsを入れたい場合、.envファイルのWORKSPACE_INSTALL_NODEtrueにすると、

.env.example
...
WORKSPACE_INSTALL_NODE=true
...
WORKSPACE_NODE_VERSION=stable
...

docker-compose.ymlでDockerfileに.envの環境変数が渡され、

docker-compose.yml
  workspace:
    build:
      context: ./workspace
      args:
       ...
        - INSTALL_NODE=${WORKSPACE_INSTALL_NODE}
       ...
        - NODE_VERSION=${WORKSPACE_NODE_VERSION}
       ...

Node.jsがインストールされます

Dockerfile
# Check if NVM needs to be installed
ARG NODE_VERSION=stable
ENV NODE_VERSION ${NODE_VERSION}
ARG INSTALL_NODE=false
ENV INSTALL_NODE ${INSTALL_NODE}
ENV NVM_DIR /home/raildock/.nvm
RUN if [ ${INSTALL_NODE} = true ]; then \
    # Install nvm (A Node Version Manager)
    curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.1/install.sh | bash && \
        . $NVM_DIR/nvm.sh && \
        nvm install ${NODE_VERSION} && \
        nvm use ${NODE_VERSION} && \
        nvm alias ${NODE_VERSION} && \
        npm install -g gulp bower vue-cli \
;fi

Dockerはsubmoduleとして追加できるようにしています。
公開しますので、ぜひ、使って見てください!
Welcome Contribute!! です!

バックエンドの話

バックエンドにRailsを使ったのは、みんながそれなりに使えそうだからという理由です。
RailsはAPIとして動作していて、そんなに話すことはありませんが、ユーザ認証のところだけ説明させていただきます

ユーザ認証にはdevise_token_authを使っています。

ApplicationControllerで

application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
end

このようにincludeしておけば

sample_controller.rb

class Api::SamplesController < ApplicationController
  before_action :authenticate_api_user!
  
  def create
    sample = current_api_user.samples.create!(sample_params)
    render json: sample, root: false
  end
end

Controllerのbefore_actionでこのようにしていすることで、tokenのないものは弾かれるようにできますcurrent_api_userでそのtokenのユーザにアクセスできるので便利ですね!

フロントエンドの話

コンパイラ

webpackerは使わず、自前でwebpackを用意しています(正確にはlaravel-mixというwebpackのラッパーを使っています)
理由は、

  1. Rails捨てたくなったときにフロントが依存しているのはよくない
  2. webpacker力不足そう、、
  3. デフォルトでapp/javascriptに作られて、その中しかみない感じが気に入らない
  4. webpackが新しいバージョンになって本当にすぐにアップデートがかかるのか心配(メンテがきちんと行われるのか)

ルートディレクトリにwebpack.mix.jsファイルをこんな感じで用意して、

webpack.mix.js
const mix = require('laravel-mix')
mix.setPublicPath('public')

mix.ts('resources/assets/ts/application.ts', 'public/assets/js')
    .sass('resources/assets/sass/application.scss', 'public/assets/css')

package.jsonのスクリプトでコンパイルします

package.json
  "scripts": {
    "dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "watch": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --watch --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
    "watch-poll": "yarn run watch -- --watch-poll",
    "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
  },

Access Token

先ほどバックエンドのところで紹介したdevise_token_authをTypeScriptの方でうまく扱ってやる必要があるので、そちらを紹介!

/auth/sign_inemailpasswordをパラメータで与えてやると、Response Headerにaccess-tokenclientuidが帰ってくるので、それをそれぞれのRequest Headerに与えてやればおkです

http.ts
import axios from 'axios';
import store2 from 'store2';

export const http = {

  async request(method : string, url : string, data : object) {
    const lowerCaseMethod = method.toLowerCase();

    try {
      return await axios.request({ url, data, method : lowerCaseMethod });
    } catch (err) {
      return Promise.reject(err);
    }
  },
 ...

  async post(url : string, data : object) {
    try {
      return await this.request('post', url, data);
    } catch (err) {
      return err;
    }
  },
 ...

  init() {
   ...

    axios.interceptors.request.use((config) => {
      config.headers['access-token'] = store2.get('access-token');
      config.headers['client'] = store2.get('client');
      config.headers['uid'] = store2.get('uid');
      return config;
    });

    axios.interceptors.response.use((response) => {
      if (response.request.responseURL === axios.defaults.baseURL + '/auth/sign_in') {
        const token = response.headers['access-token'] || response.data['access-token'];
        token && store2.set('access-token', token);
      ...
      }
      return response;
    },                              (error) => {
      return Promise.reject(error);
    });
  },
};

sign_inリクエストを送った時に、Response Headerから先ほどの値を取り出し、localStorageに保存、リクエストを送る時にはlocalStorageからそれぞれの値を取り出し、Request Headerに付与しています

Vue.jsとTypeScript

Vue.jsはSPAとして動作しています。
先に使ったライブラリをまとめます

名前 概要
vue-router routingを行う
vuex コンポーネント間のデータ通信
vue-class-component コンポーネントをクラスベースで定義するときに使う
vuex-class vue-class-componentで定義したコンポーネントにvuexをinjectするときに使う
vue-style-loader vue.js内でScssを使うときに使います

vue-router

router.ts
import Vue from 'vue';
import VueRouter from 'vue-router';

import Home from './views/home/Home.vue';

Vue.use(VueRouter);

const router = new VueRouter({
  routes: [
    {
      path: '/',
      component: Home,
      name: 'home',
      meta: { requiresAuth: false },
    },
  ]
})

router.beforeEach(async (to, from, next) => {
  const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
  if (requiresAuth) {
    try {
      await // トークンの確認
      next();
    } catch (err) {
      next('/');
    }
  } else {
    next();
  }
});

export default router;

Home.vueのようにそれぞれのviewをimportし、routesの中に入れていきます
metaタグにrequiresAuthを付与しておき、trueの場合は、router.beforeEachの中でトークンを所持しているかの確認を行なっています

vuex

難しいので、詳細は https://vuex.vuejs.org/ja/getting-started.html をご覧ください

それぞれの役割は
state : データストレージ
getters : コンポーネントからアクセスするときのgetter
actions : 非同期を含めた処理を行うことができ、コンポーネントから呼び出すことができる
mutations : stateの状態変更の際に使う(非同期処理などはダメ)actionからはcommit('mutation名', data)で呼び出すことができる
こんな感じです

例えば、ユーザの情報をコンポーネント間で共有したい時

user.ts
import { http } from '@/api/http';
import { userStub } from '@/stubs/user';

const state = {
  me: userStub
};

const getters = {
  me: (state: any) => state.me,
};

const actions = {
 ...
  async signIn ({ commit } : any, data : object) {
    try {
      const response = await http.post('/auth/sign_in', data);
      commit('changeProfile', response.data);
    } catch (error) {
      return Promise.reject(error);
    }
  },
 ...
};

const mutations = {
  changeProfile (state: any, me : object) {
    state.me = me;
  },
};

export default {
  state,
  getters,
  actions,
  mutations,
};

こんな感じになります

vue-class-componentとvuex-classの話

Vue コンポーネントオプション内部で TypeScript が型を適切に推測できるようにするには、Vue.component または Vue.extend でコンポーネントを定義する必要があります:
From : https://jp.vuejs.org/v2/guide/typescript.html#%E5%9F%BA%E6%9C%AC%E7%9A%84%E3%81%AA%E4%BD%BF%E3%81%84%E6%96%B9

なので、公式にメンテナンスされてるvue-class-componentを使うのが一般的です
また、vuex-classは先ほど、vuexで実装したactionやmutation、getterなどをvue-class-component内にinjectすることができます

以下が使用例です

AwesomeUser.vue
<template>
    ...
</template>

<script lang="ts">
    import Vue from 'vue'
    import Component from 'vue-class-component'
    import { Getter, Action } from 'vuex-class'
    import { userStub } from '@/stubs/user';

    @Component
    export default class AwesomeUser extends Vue {
        @Getter me : object
        @Action signIn : any
        user = userStub

        async created () {
            this.user = await this.signIn()
            console.log(this.user)
            console.log(this.me)
        }
    }
</script>

vue-class-component@Component decoratorをexportするclassにつければokです
vuex-class@Getter@Actiondecoratorをつければinjectさせることができます

Sassの話

CSSのフレームワークとして、Blumaを使っています
今回、jQueryは入れたくなかったので、jQuery依存が全くないCSSフレームワークを選定しています

ブロックチェーンの話

ブロックチェーン(&分散アプリケーション)のメリットは以前の記事をご覧くださいhttps://qiita.com/anneau/items/3be8cd7cd8c34e28f90a

開発手法に関しても以前、記事を書かせていただきましたので、こちらをご覧くださいhttps://qiita.com/anneau/items/3a65691c8870761292ee

現在、Ethereumを用いた多くのサービスはだいたいこのような構成になっていると思います。

ブロックチェーンで管理されたデータは分散され、改ざんすることはできず、また、消えることはないという素晴らしい機能を備えていますが、まだまだ足りていない機能が多いです(検索機能など、、)、また、今回、社内ツールでは、プライベートなEthereum環境で動かすので、関係ないですが、一般的にはあまり多くの処理をさせると、それだけのコスト(手数料)がかかります。
そのため、普通のデータはRailsなどのアプリケーションを介し、DBに保存し、改ざんを防ぎたいデータ(仮想通貨の取引履歴などはまさにそうですね)はブロックチェーンに記録します

社員のアカウントを作る時、一緒にEthereumのアカウントも作成するようにして、アカウントをRailsのDBに入れておきます

SignUp.vue
<template>
    ...
</template>

<script lang="ts">
    @Action register : any
    ...
    async created () {
        const address = await window.web3.eth.personal.newAccount(this.password)
        const response = await this.register(
                    { name: this.name, email: this.email, password: this.password, ethereum_address: address })
    }
</script>

そのアカウントを用い、目的のアカウントに仮想通貨を送るようにします

仮想通貨の移動は開発手法の記事で書いたものをそのまま使ってますので、そちらを参照してください

結び

長々と失礼しました
省略している部分も多いので、わかりづらいところがありましたら、コメントでお願いします。

余談

Ethereumのアカウントを作った時

const address = await window.web3.eth.personal.newAccount(this.password)

アドレスしか帰ってきていないことに違和感を感じた人はいますでしょうか?

Coincheckの事件が記憶に新しいですが、一般的に、Ethereumを含め、Blockchainサービスにアカウントを作った時、秘密鍵が帰ってきて、それで署名を行い取引を行います。
もちろんEthereumにも用意されており、
http://web3js.readthedocs.io/en/1.0/web3-eth-accounts.html#create
ここにあるように

web3.eth.accounts.create([entropy]);

とすれば、privateKeyが帰ってくるようになっています
それで署名を行い、取引を行うことが一般的です

今回そうしなかったのは、TypeScriptの型定義ファイルが間違っていて、使えなかったからです

プルリクがマージされたので、きっとそのうちnpmに上がってTypeScriptからも使えるようになると思います。
急ぎの方は、submoduleで入れましょう!
もちろん、privateKeyはコールドウォレットで管理しましょう 笑

ではでは〜

終わり

260
243
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
260
243

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?