CodiMDで社内簡易Qiitaっぽいものが作れるので、練習がてら作ってみた話

まえおき

アドベントカレンダーでCW社のイケイケエンジニアが、なんかおもしろそうな記事を発信していた。

qiita.com

HackMD自体は使ってたことはあるんだけど、CW社ではQiita Teamが自由に使えていたので、自分用のメモにわざわざHackMDを使うことはしていなかった。

ただ、今の会社に転職してから、Qiita Teamは使えないし、社内のドキュメント共有で使っているAtlassian Confluenceは使い勝手が最低だし、HackMDいいんじゃね?と思って自分用インスタンスを立ててみることにした。

セットアップで少しだけハマった

config.json.example というものを config.jsonにコピーして使うんだけど、config.json.exampleにあるもので必要のないSAML設定などは消さないと動かなかったり、逆に protocolUseSSL などは当然ほとんどの人は使うはずなのにexampleにはなく自分で書き足さないといけなかったり、... あとは、データベースの設定が db っていうのだと動かなくて、 dbURLっていうのを指定しないといけなかったり...w

ともかく、exampleがほとんど信用にならなくて、 CodiMD Documentation - HackMD ここにある設定値を、冒頭で書いたアドベントカレンダー記事を参考にしつつ書き足していく必要があった。

最終的な運用で使うことになったconfig.jsonは以下の通り、ほぼexampleの跡形はないものであるw

{
    "production": {
        "domain": "ひみつ.japaneast.cloudapp.azure.com",
        "protocolUseSSL": true,
        "useSSL": false,
        "sessionSecret": "ひみつ",
        "loglevel": "info",
        "host": "localhost",
        "port": 8080,
        "allowAnonymous": false,
        "allowAnonymousEdits": false,
        "allowAnonymousViews": false,
        "defaultPermission": "private",
        "email": false,
        "allowEmailRegister": false,
        "allowGravatar": true,

        "dbURL": "mysql://hackmd:ひみつ@127.0.0.1:3306/hackmd",
        "bitbucket": {
            "clientID": "ひみつ",
            "clientSecret": "ひみつ",
            "emailDomain": "ひみつ"
        },

        "imageUploadType": "azure",
        "azure":
        {
          "connectionString": "ひみつ",
          "container": "ひみつ"
        },
        "plantuml":
        {
          "server": "https://www.plantuml.com/plantuml"
        },
        "linkifyHeaderStyle": "gfm"
    }
}

ちなみに、今回は勉強がてら自前でAzure VMDebian上でいろいろインストールしてCodiMDを入れたので、config.jsonでハマったわけだが、普通のユースケースではHerokuを使えば十分なはずなので、こんなハマり方はきっとしないだろう。

Bitbucketログインは無かった...

今回は、Atlassian Cloudの会社用アカウントでCodiMDを使えるのがいい。なぜかというと、「コンフルの糞め!」と思った人をいずれ取り込みたいから。

しかしながら,世の中的にBitbucketアカウントを使う人は少ないのか、CodiMDは(GitHubやGitLabはサポートしていたが)Bitbucketアカウントでのログインはサポートされていなかった。

とりあえず練習がてらGitLabのソースコードをコピペして、Bitbucketログイン機能を作った。

github.com

(いい感じでレビューしてもらえて、たぶん次のバージョンでは使えるようになるらしい。CodiMDのスピード感すごいな...)

ソースコードが役割ごとによく整理されていて、grepもしやすかったので、Node.jsはコピペプログラマな自分でも機能追加するのはかなり簡単だった。良いソースは人を育てるとはこういうことなのか。

社外の人を見れなくするには

たとえばoauth2_proxyを使うと、特定のOrganizationに属してる人じゃなければ403エラーにすることができる。

でもCodiMDには現時点でそんな機能はない。GitHubGoogleログインでもそういう機能はないらしく

github.com

github.com

それぞれにissueがあがっている状態である。

ただ、流石に社内メモを社外に漏らすのはダメだ。汚くてもいいから作らないといけない。

// bitbucket auth callback
bitbucketAuth.get('/auth/bitbucket/callback',
  passport.authenticate('bitbucket', {
    successReturnToOrRedirect: config.serverURL + '/',
    failureRedirect: config.serverURL + '/'
  })
)

ここのところで、特定のEメールアドレスじゃない人はfailureRedirectのほうに行くようにすると、とりあえずは良さそう。

で、早速passportのソースコードを見てみる。

github.com

これまた最高に読みやすい。 req.logIn() ってところで失敗ルートに流せば良さそうである。しかしながらそのlogInっていうメソッド定義は

passport/request.js at 42ff63c60ae55f466d21332306e9112295c0535e · jaredhanson/passport · GitHub

本当にログイン処理しかしていない。となると、選択肢は2つしかない

  • 自前でログイン後の処理を(successReturnToOrRedirectに頼らず)実装する
  • 汚くてもいいから passport-bitbucket-oauth2 を改造して、特定メールアドレス以外はエラーにしてしまう

今回の場合(Bitbucketログイン)だと、そもそもpassport-bitbucket-oauth2がユーザのemailを取得しに行ってくれていなかったという問題もあり、短手番でやるためにpassport-bitbucket-oauth2をいじることにした。

Strategy.prototype.userProfile = function(accessToken, done) {
  var emailDomain = this._emailDomain;
  var oauth2 = this._oauth2;
  oauth2.get(this._userProfileURL, accessToken, function (err, body, res) {
    var json;

    if (err) {
      return done(new InternalOAuthError('Failed to fetch user profile', err));
    }

    try {
      json = JSON.parse(body);
    } catch (ex) {
      return done(new Error('Failed to parse user profile'));
    }

    var profile = Profile.parse(json);
    profile.provider  = 'bitbucket';
    profile._raw = body;
    profile._json = json;

    if (!emailDomain) {
      return done(null, profile);
    }

    oauth2.get("https://api.bitbucket.org/2.0/user/emails", accessToken, function (err, body, res) {
      try {
        json = JSON.parse(body);
      } catch (ex) {
        return done(new Error('Failed to parse user emails'));
      }

      /* 以下のような構造のJSONが返ってくる。
      [
        {
          "is_primary": true,
          "is_confirmed": true,
          "type": "email",
          "email": "iwaki@yusuke-iwaki.com",
          "links": {
            "self": {
              "href": "https://api.bitbucket.org/2.0/user/emails/iwaki@yusuke-iwaki.com"
            }
          }
        }
      ]
      */
      var primaryEmail = json.values.find(function(email) { return email.is_primary && email.is_confirmed });
      console.log("primaryEmail:", primaryEmail)
      if (!primaryEmail || !primaryEmail.email) {
        return done(new Error('Primary email is not found.'));
      }
      if (primaryEmail.email.split("@")[1] != emailDomain) {
        return done(new Error('Primary Email address does not belongs to @' + emailDomain));
      }

      done(null, profile);
    });
  });
};

いやー、死ぬほど汚い。けど、とりあえず目的は果たせた。

f:id:YusukeIwaki:20191230153328p:plain

社外の人がBitbucketログインしても、500エラーになる。

500エラー・・・。まぁダメだとはわかってるんだけど、とりあえず社外の人を弾ければ何でもいいんだ。まずは自分しか使わないし。

そんなわけで、使ってみるとまるでQiita/Kobito

kobitoがサービス終了して久しいが、それに近い使用感でメモが取れるのは本当に良い。

Confluenceは使い勝手が最悪なので、自分の作業メモは今後CodiMD使うようになると思う。

まとめ

無理してConfluenceで消耗しているくらいなら、CodiMDを適当に建てて使うほうが絶対に良い。