読者です 読者をやめる 読者になる 読者になる

nirasan's tech blog

趣味や仕事の覚え書きです。Linux, Perl, PHP, Ruby, Javascript, Android, Cocos2d-x, Unity などに興味があります。

Go 言語で gorilla/mux, gorilla/context, gorilla/sessions を使ったユーザーログイン機能のサンプル

はじめに

Go 言語練習のため gorilla/mux, gorilla/context, gorilla/sessions を使ったユーザーログイン機能を実装してみました。
ユーザーの管理はメモリ上の map 型変数にユーザー名とパスワードを入れているだけなので、ここをDBに変えるのが次の課題のつもりです。

package main

import (
	"net/http"
	"html/template"
	"github.com/gorilla/mux"
	"github.com/gorilla/context"
	"github.com/gorilla/securecookie"
	"github.com/gorilla/sessions"
	"errors"
)

var (
	// セッションストアの初期化
	store *sessions.CookieStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
	// 登録ユーザーをメモリ上で管理
	users map[string]string = make(map[string]string)
)

const (
	SessionName = "session-name"
	ContextSessionKey = "session"
)

func main() {
	r := mux.NewRouter()
	handleFunc(r, "/", rootHandler)
	handleFunc(r, "/register", registerGetHandler).Methods("GET")
	handleFunc(r, "/register", registerPostHandler).Methods("POST")
	handleFunc(r, "/login", loginGetHandler).Methods("GET")
	handleFunc(r, "/login", loginPostHandler).Methods("POST")
	handleFunc(r, "/logout", logoutGetHandler).Methods("GET")
	handleFunc(r, "/logout", logoutPostHandler).Methods("POST")
	http.ListenAndServe(":8080", r)
}

// アプリケーション共通処理を常に呼び出すための糖衣構文
func handleFunc(r *mux.Router, path string, fn http.HandlerFunc) *mux.Route {
	return r.HandleFunc(path, applicationHandler(fn))
}

// アプリケーション共通処理
func applicationHandler(fn http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// セッションの取得
		session, err := store.Get(r, SessionName)
		if err != nil {
			// 不正なセッションだった場合は作り直す
			session, err = store.New(r, SessionName)
			checkError(err)
		}
		context.Set(r, ContextSessionKey, session)
		// 個別のハンドラー呼び出し
		fn(w, r)
	}
}

// トップページ
func rootHandler(w http.ResponseWriter, r *http.Request) {
	type page struct {
		Username string
	}
	p := &page{}
	session, err := getSession(r)
	checkError(err)
	if v, ok := session.Values["username"]; ok {
		p.Username = v.(string)
	}
	executeTemplate(w, "template/index.html", p)
}

// 登録ページ
func registerGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/register.html", nil)
}

// 登録処理
func registerPostHandler(w http.ResponseWriter, r *http.Request) {
	// フォームのパース
	r.ParseForm()
	// フォームの値の取得
	username, password := r.Form["username"][0], r.Form["password"][0]
	// 空なら登録ページへ
	if username == "" || password == "" {
		http.Redirect(w, r, "/register", http.StatusFound)
		return
	}
	// ユーザー登録
	users[username] = password
	// 現在のユーザーをセッションで管理
	session, err := getSession(r)
	checkError(err)
	session.Values["username"] = username
	session.Save(r, w)
	// トップページへ
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

// ログインページ
func loginGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/login.html", nil)
}

// ログイン処理
func loginPostHandler(w http.ResponseWriter, r *http.Request) {
	// フォームのパース
	r.ParseForm()
	// フォームの値の取得
	username, password := r.Form["username"][0], r.Form["password"][0]
	// 空ならログイン画面へ
	if username == "" && password == "" {
		http.Redirect(w, r, "/login", http.StatusFound)
		return
	}
	// ユーザーが登録済みでありパスワードが一致したらログイン状態に
	if v, ok := users[username]; ok && v == password {
		session, err := getSession(r)
		checkError(err)
		session.Values["username"] = username
		session.Save(r, w)
		http.Redirect(w, r, "/", http.StatusSeeOther)
	} else {
		http.Redirect(w, r, "/login", http.StatusSeeOther)
	}
}

// ログアウトページ
func logoutGetHandler(w http.ResponseWriter, r *http.Request) {
	executeTemplate(w, "template/logout.html", nil)
}

// ログアウト処理
func logoutPostHandler(w http.ResponseWriter, r *http.Request) {
	session, err := getSession(r)
	checkError(err)
	delete(session.Values, "username")
	session.Save(r, w)
	http.Redirect(w, r, "/", http.StatusSeeOther)
}

// セッションの取得
func getSession(r *http.Request) (*sessions.Session, error) {
	if v := context.Get(r, ContextSessionKey); v != nil {
		return v.(*sessions.Session), nil
	}
	return nil, errors.New("failed to get session")
}

// テンプレートの実行
func executeTemplate(w http.ResponseWriter, name string, data interface{}) {
	t, err := template.ParseFiles(name)
	checkError(err)
	err = t.Execute(w, data)
	checkError(err)
}

// エラーチェック
func checkError(err error) {
	if err != nil {
		panic(err)
	}
}

おわりに

net/http のハンドラーをチェインさせて共通処理を切り出すパターンは「Go言語によるWebアプリケーション開発」読んだらちゃんと書いてあったのでちゃんと読もうと思いました。
学習目的で gorilla/mux で実装してみてて実際勉強になってますが、普通に web アプリ作るならたぶん goji とか WAF を使った方がいい気がしてきてます。

Go 言語で gorilla/mux を使った簡単なウェブアプリのサンプル

はじめに

Go 言語で簡単なウェブアプリを作りたいので軽量なウェブツールキット gorilla/mux を使ったサンプル。
context の使い方は gin や goji のミドルウェアの実装を参考に、共通化したい処理を個別のハンドラの上にラップする形にした。おきまりのパターンっぽいので Usage とかに書いて欲しい。

package main

import (
	"fmt"
	"net/http"
	"github.com/gorilla/mux"
	"github.com/gorilla/context"
)

func main() {
	r := mux.NewRouter()

	// 単純なハンドラ
	r.HandleFunc("/", YourHandler)

	// パスに変数を埋め込み
	r.HandleFunc("/hello/{name}", VarsHandler)

	// パス変数で正規表現を使用
	r.HandleFunc("/hello/{name}/{age:[0-9]+}", RegexHandler)

	// クエリ文字列の取得
	r.HandleFunc("/hi/", QueryStringHandler)

	// 静的ファイルの提供
	// $PROROOT/assets/about.html が http://localhost:8080/assets/about.html でアクセスできる
	r.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(http.Dir("./assets"))))

	// リダイレクト
	r.HandleFunc("/moved", RedirectHandler)

	// マッチするパスがない場合のハンドラ
	r.NotFoundHandler = http.HandlerFunc(NotFoundHandler)

	// 複数のハンドラで共通の処理を実行する
	// 今回はcontextのセットとゲットを試しているが、同じパターンでDBの初期化や認証処理やログ書き出しなどにも応用できる
	// ハンドラを引き渡すには http.Handler 型を使い func(http.ResponseWriter, *http.Request) から http.Handler への変換には http.HandlerFunc を利用する
	// http.Handler をハンドラとして登録する場合は Router.Handle を利用する
	r.Handle("/some1", UseContext(http.HandlerFunc(SomeHandler1)))
	r.Handle("/some2", UseContext(http.HandlerFunc(SomeHandler2)))

	// http://localhost:8080 でサービスを行う
	http.ListenAndServe(":8080", r)
}

func YourHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Gorilla!\n"))
}

func VarsHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	fmt.Fprintf(w, "%s Loves Gorilla\n", vars["name"])
}

func RegexHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)
	fmt.Fprintf(w, "%s is %s years old\n", vars["name"], vars["age"])
}

func QueryStringHandler(w http.ResponseWriter, r *http.Request) {
	q := r.URL.Query()
	fmt.Fprintf(w, "%s Loves Gorilla\n", q.Get("name"))
}

func RedirectHandler(w http.ResponseWriter, r *http.Request) {
	http.Redirect(w, r, "/", http.StatusFound)
}

func NotFoundHandler(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Gorilla!\nNot Found\n"))
}

const MyContextKey = 1

func UseContext(handler http.Handler) http.Handler {
	fn := func(w http.ResponseWriter, r *http.Request) {
		context.Set(r, MyContextKey, "Call SomeMiddleware")
		handler.ServeHTTP(w, r)
	}
	return http.HandlerFunc(fn)
}

func SomeHandler1(w http.ResponseWriter, r *http.Request) {
	contextVal := context.Get(r, MyContextKey)
	fmt.Fprintf(w, "%s Call SomeHandler1", contextVal)
}

func SomeHandler2(w http.ResponseWriter, r *http.Request) {
	contextVal := context.Get(r, MyContextKey)
	fmt.Fprintf(w, "%s Call SomeHandler2", contextVal)
}

Mac + DockerToolbox 環境の Centos6 で Supervisor を使って複数のサービスを起動する 〜 sshd と named を起動する例

はじめに

  • Docker のコンテナでは複数のサービスを同時に立ち上げられないので、任意のプログラムをデーモン化して起動や停止を管理する Supervisor を利用して、Supervisor 経由で複数サービスを管理する。
  • Supervisor のパッケージは epel にあるが、バージョンが古く、サービスの再起動時にやたら時間がかかるなど動作がおかしいので、python のインストールツール easy_install 経由で導入する。

Dockerfile の用意

FROM centos:6

### sshd のインストール
RUN yum -y install initscripts MAKEDEV
RUN yum check
RUN yum -y update
RUN yum -y install openssh-server passwd

### sshd の設定
RUN sed -ri 's/^#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config
RUN sed -ri 's/^#PermitRootLogin yes/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -ri 's/^UsePAM yes/UsePAM no/' /etc/ssh/sshd_config

### sshd の起動準備
RUN /etc/init.d/sshd start
RUN /etc/init.d/sshd stop

### パスワードなしで接続
RUN passwd -d root

### named のインストール
RUN yum install -y bind
RUN /etc/init.d/named start
RUN /etc/init.d/named stop

### Supervisor のインストール
RUN yum install -y wget
RUN wget https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py -O - | python
RUN easy_install supervisor
COPY supervisord.conf /etc/supervisord.conf

CMD /usr/bin/supervisord -c /etc/supervisord.conf

Supervisor の設定ファイルの用意

  • Dockerfile と同じディレクトリに supervisord.conf として用意
[inet_http_server]
port=127.0.0.1:9001

[supervisord]
logfile=/var/log/supervisord.log ; (main log file;default $CWD/supervisord.log)
logfile_maxbytes=50MB       ; (max main logfile bytes b4 rotation;default 50MB)
logfile_backups=10          ; (num of main logfile rotation backups;default 10)
loglevel=info               ; (logging level;default info; others: debug,warn)
pidfile=/var/run/supervisord.pid ; (supervisord pidfile;default supervisord.pid)
nodaemon=true              ; (start in foreground if true;default false)
minfds=1024                 ; (min. avail startup file descriptors;default 1024)
minprocs=200                ; (min. avail process descriptors;default 200)

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[supervisorctl]
serverurl=http://127.0.0.1:9001

[program:sshd]
command=/usr/sbin/sshd -D
autostart=true
autorestart=true

[program:named]
command=/usr/sbin/named -u named -f
process_name=%(program_name)s
numprocs=1
directory=/var/named
priority=100
autostart=true
autorestart=true
startsecs=5
startretries=3
exitcodes=0,2
stopsignal=TERM
stopwaitsecs=10
redirect_stderr=false
stdout_logfile=/var/log/named_supervisord.log
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=10
stdout_capture_maxbytes=1MB

イメージの作成

$ docker build -t centos:supervisor .

コンテナの起動

$ docker run -p 20022:22 -p 20053:53 -d centos:supervisor

接続確認

$ ssh -p 20022 root@127.0.0.1

サービスの管理

  • コンテナ上で実行
$ supervisorctl status #=> サービスの確認
$ supervisorctl restart named #=> サービスの再起動

Mac + DockerToolbox で ssh 可能な CentOS のイメージを作成する

DockerToolbox のインストール

http://docs.docker.com/mac/step_one/ の手順に従ってインストール

Docker Quickstart Terminal の実行

Dockerfile の作成

  • Docker 用のディレクトリを作成して Dockerfile の作成
$ mkdir -p /path/to/docker/centos-ssh
$ cd /path/to/docker/centos-ssh
FROM centos:6

RUN yum -y install initscripts MAKEDEV

RUN yum check

RUN yum -y update

RUN yum -y install openssh-server passwd

# 空パスワードの場合は以下をコメントアウト
RUN sed -ri 's/^#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config

RUN sed -ri 's/^#PermitRootLogin yes/PermitRootLogin yes/' /etc/ssh/sshd_config
RUN sed -ri 's/^UsePAM yes/UsePAM no/' /etc/ssh/sshd_config

RUN /etc/init.d/sshd start
RUN /etc/init.d/sshd stop

# 空パスワードの場合は以下をコメントアウト
RUN passwd -d root

# 任意のパスワードの場合は以下をコメントアウト & パスワードを書き換える
# RUN echo 'root:root' | chpasswd

EXPOSE 22
CMD /usr/sbin/sshd -D

イメージの作成

$ docker build -t centos-ssh .

コンテナの起動

$ docker run -p 20022:22 -d centos-ssh

ポートフォワーディングの設定

  • コンテナ起動時に -p オプションで、コンテナから docker-machine の間のポートフォワーディングの設定を行った
  • Mac の場合はさらに、docker-machine と Mac の間のポートフォワーディングの設定を行う必要がある

手順

  • アプリケーションから VirtualBox の起動
  • "default" を選択して "設定" ボタンを押下
  • "ネットワーク" を選択して "ポートフォワーディング" を押下
  • ポートフォワード設定の追加

ssh 接続実行

$ ssh -p 20022 root@127.0.0.1

Rails で作った API サーバーに Unity でリクエストをする

はじめに

サーバー側の変更点

  • Doorkeeper でアクセストークンの更新に対応するため config/initializers/doorkeeper.rb に以下を追記する。
  use_refresh_token

スクリプト

  • 任意のオブジェクトに以下のスクリプトをアタッチして使用する
  • AuthorizedRequest メソッドに URL とリクエスト成功時のコールバック関数などを指定すると、自動で認証を行った上でリクエストを送信してくれる。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using MiniJSON;
using System;

public class APIManager : MonoBehaviour {

	public string TokenUrl = "http://127.0.0.1:3000/oauth/token";
	public string UsersUrl = "http://127.0.0.1:3000/users/100";

	public string client_id = "07461c1f68870ea090ed20af49d0680782950119213e2a1af9e1f653c688dca3";
	public string client_secret = "20dea83f022c9e2a0e6fbec1da0e9c0e20c40e1bf09d2e8db029b461653a88bd";

	public string username = "user1@example.com";
	public string password = "password";

	public string AccessToken;
	public long ExpiresIn;
	public long CreatedAt;
	public string RefreshToken;

	private static readonly DateTime UNIX_EPOCH = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);	
	public DateTime ExpiresAt;

	void Start () {
		StartCoroutine(GetUsers());
	}

	/// <summary>
	/// 使用例としてユーザー情報を取得する
	/// </summary>
	public IEnumerator GetUsers () {

		yield return StartCoroutine(AuthorizedRequest(
			url: UsersUrl, 
			onSuccess: (WWW www) => {
				Debug.Log(www.text);
				Debug.Log(www.error);
				foreach (var kv in www.responseHeaders) {
					Debug.Log(string.Format("{0} : {1}", kv.Key, kv.Value));
				}
			}
		));
	}

	/// <summary>
	/// 認証が必要なページにHTTPリクエストを送信する
	/// アクセストークンが未取得であれば取得し
	/// アクセストークンが期限切れであれば更新する
	/// </summary>
	/// <param name="url">URL.</param>
	/// <param name="form">Form.</param>
	/// <param name="headers">Headers.</param>
	/// <param name="onSuccess">On success.</param>
	/// <param name="onError">On error.</param>
	public IEnumerator AuthorizedRequest (string url, WWWForm form = null, Dictionary<string, string> headers = null, System.Action<WWW> onSuccess = null, System.Action onError = null) {

		if (string.IsNullOrEmpty(AccessToken)) {

			var tokenGet = TokenGet();
			yield return StartCoroutine(tokenGet);

			if (Request.RequestIsError(tokenGet)) {
				yield break;
			}
		}

		if (ExpiresAt < DateTime.Now) {

			var tokenRefresh = TokenRefresh();
			yield return StartCoroutine(tokenRefresh);

			if (Request.RequestIsError(tokenRefresh)) {
				yield break;
			}
		}

		if (headers == null) {
			headers = new Dictionary<string, string>();
			headers.Add("Authorization", string.Format("Bearer {0}", AccessToken));
		}

		var request = new Request(url, form, headers, onSuccess, onError);
		yield return StartCoroutine(request.Send());
		yield return request;
	}

	/// <summary>
	/// アクセストークンを取得する
	/// </summary>
	IEnumerator TokenGet () {

		var form = new WWWForm();
		form.AddField("grant_type", "password");
		form.AddField("username", username);
		form.AddField("password", password);
		form.AddField("client_id", client_id);
		form.AddField("client_secret", client_secret);

		var request = new Request(url: TokenUrl, form: form, onSuccess: TokenUpdate);
		yield return StartCoroutine(request.Send());
		yield return request;
	}

	/// <summary>
	/// アクセストークンを更新する
	/// </summary>
	IEnumerator TokenRefresh () {

		var form = new WWWForm();
		form.AddField("grant_type", "refresh_token");
		form.AddField("refresh_token", RefreshToken);
		form.AddField("client_id", client_id);
		form.AddField("client_secret", client_secret);

		var request = new Request(url: TokenUrl, form: form, onSuccess: TokenUpdate);
		yield return StartCoroutine(request.Send());
		yield return request;
	}

	/// <summary>
	/// アクセストークン取得リクエストのレスポンスからアクセストークン情報を取得しインスタンス変数に格納する
	/// </summary>
	/// <param name="www">Www.</param>
	private void TokenUpdate (WWW www) {
		var jsonDict = Request.WWWToJson(www);

		AccessToken = (string)jsonDict["access_token"];
		ExpiresIn = (long)jsonDict["expires_in"];
		CreatedAt = (long)jsonDict["created_at"];
		RefreshToken = (string)jsonDict["refresh_token"];

		ExpiresAt = UNIX_EPOCH.AddSeconds(CreatedAt + ExpiresIn).ToLocalTime();
	}

	private class Request {

		public bool IsError { private set; get; }

		private float TIMEOUT = 5f;

		private string url;
		private byte[] formdata;
		private Dictionary<string, string> headers;
		private System.Action<WWW> onSuccess;
		private System.Action onError;

		public Request (string url, WWWForm form = null, Dictionary<string, string> headers = null, System.Action<WWW> onSuccess = null, System.Action onError = null) {
			this.url = url;
			this.formdata = form == null ? null : form.data;
			this.headers = headers;
			this.onSuccess = onSuccess == null ? (WWW www) => {} : onSuccess;
			this.onError = onError == null ? () => {} : onError;
		}

		public IEnumerator Send () {

			/*
			Debug.Log(url);
			if (headers != null) {
				foreach (var kv in headers) {
					Debug.Log(string.Format("{0} : {1}", kv.Key, kv.Value));
				}
			}
			*/

			var www = new WWW(url, formdata, headers);
			var endtime = Time.realtimeSinceStartup + TIMEOUT;

			while (!www.isDone || Time.realtimeSinceStartup < endtime) {
				yield return null;
			}

			if (WWWIsError(www)) {
				this.IsError = true;
				onError();
			} else {
				this.IsError = false;
				onSuccess(www);
			}

			/*
			Debug.Log(www.text);
			Debug.Log(www.error);
			foreach (var kv in www.responseHeaders) {
				Debug.Log(string.Format("{0} : {1}", kv.Key, kv.Value));
			}
			*/

			yield return this;
		}

		private bool WWWIsError (WWW www) {
			return !string.IsNullOrEmpty(www.error);
		}

		public static Dictionary<string, object> WWWToJson (WWW www) {
			return Json.Deserialize(www.text) as Dictionary<string, object>;
		}

		public static bool RequestIsError (IEnumerator request) {
			return ((Request)request.Current).IsError;
		}
	}
}

Rails で API サーバーの認証の仕組みを作る

はじめに

  • Rails でモバイルアプリのバックエンドに使う API サーバーのユーザー作成と認証の仕組みを作ったメモです
  • Doorkeeper + Sorcery を使った認証の構築とテストまで記述します

導入

新しいプロジェクトを作成
$ rails new -T -B doorkeeper-test

gem のインストール

デフォルトの Gemfile に以下を追記
gem 'doorkeeper'
gem 'sorcery'
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_girl_rails'
  gem 'database_rewinder'
end
インストール
$ bundle install

Sorcery のインストール

# Sorcery の設定ファイルと Sorcery に対応した User モデルの生成
$ rails generate sorcery:install

Doorkeeper のインストール

# 設定ファイルの作成とルートの追加
$ rails generate doorkeeper:install
# マイグレーションファイルの作成
$ rails generate doorkeeper:migration

マイグレーションの実行

$ rake db:migarate

Doorkeeper の設定

Doorkeeper.configure do
  # Change the ORM that doorkeeper will use (needs plugins)
  orm :active_record

  resource_owner_authenticator do
    User.find_by_id(session[:current_user_id]) || redirect_to(login_url)
  end

  resource_owner_from_credentials do
    User.authenticate(params[:username], params[:password])
  end
end
Doorkeeper.configuration.token_grant_types << "password"

ユーザーコントローラの作成

  • ユーザーの作成とユーザー情報表示ページを作成する
  • ユーザー作成は認証なしで、ユーザー情報表示は認証必須のページにする
generate
$ rails g controller users create show
routes.rb
  resources :users, only: [:create, :show]
app/controllers/users_controller.rb
class UsersController < ApplicationController
  before_action :doorkeeper_authorize!, only: [:show] # show のみ認証が必要

  def create
    @user = User.new(user_params)
    if @user.save
      render json: @user
    else
      head :bad_request
    end
  end

  def show
    render json: User.find(params[:id])
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end
end
app/controllers/application_controller.rb
  • CSRF 対策を無効にする
-  protect_from_forgery with: :exception
+  protect_from_forgery with: :null_session

Doorkeeper の認証用キーの作成

動作確認

  • curl コマンドで動作確認を行う
ユーザーの作成
$ curl -F "user[email]=user1@example.com" -F "user[password]=password" http://127.0.0.1:3000/users
アクセストークンの取得
  • client_id には「Doorkeeper の認証用キーの作成」で作成した Application Id を、client_secret には同じく Secret を指定する
$ curl -F "grant_type=password" \
       -F "client_id=07461c1f68870ea090ed20af49d0680782950119213e2a1af9e1f653c688dca3" \
       -F "client_secret=20dea83f022c9e2a0e6fbec1da0e9c0e20c40e1bf09d2e8db029b461653a88bd" \
       -F "username=user1@example.com" -F "password=password" \
       http://127.0.0.1:3000/oauth/token.json
ユーザーの取得
  • Bearer 以降には「アクセストークンの取得」で取得したトークンの文字列を指定する
$ curl -H "Authorization: Bearer 18afa04c80fbe3528b5d495c24e8badcbaeee12b2866d22d8f220c44543ae01c" http://127.0.0.1:3000/users/1

テスト

rspec の準備
$ rails g rspec:install
rails_helper.rb に追記
  config.before :suite do
    DatabaseRewinder.clean_all
  end

  config.after :each do
    DatabaseRewinder.clean
  end

  config.before :all do
    FactoryGirl.reload
  end
specs_helper.rb に追記
  require 'factory_girl_rails'
  config.include FactoryGirl::Syntax::Methods
factories/users.rb の作成
FactoryGirl.define do
  factory :user do
    sequence(:email) {|n| "user#{n}@example.com" }
    password "password"
  end
end
factories/doorkeeper.rb の作成
FactoryGirl.define do
  factory :access_token, class: Doorkeeper::AccessToken do
    sequence(:resource_owner_id) { |n| n }
    application
    expires_in 1.hours
  end

  factory :application, class: Doorkeeper::Application do
    sequence(:name){ |n| "Application #{n}" }
    redirect_uri 'https://example.com/callback'
  end
end
spec/controllers/user_controller_spec.rb
require 'rails_helper'

RSpec.describe UsersController, type: :controller do
  describe "GET #show" do
    let!(:application) { create :application }
    let!(:user)        { create :user }
    let!(:token)       { create :access_token, :application => application, :resource_owner_id => user.id }
    it "returns http success" do
      get :show, {format: :json, access_token: token.token, id: user.id}
      expect(response).to have_http_status(:success)
    end
  end
end

さいごに

  • クライアントからは Doorkeeper のトークン情報を見れる必要はないから、中継用のアクションとかを用意したほうがいいかな?

Rails初期化チートシート

プロジェクトの作成

rails new PROJECT_NAME -T -B
cd PROJECT_NAME

git の設定

git init
curl https://www.gitignore.io/api/rails,ruby,osx,linux,vim,sublimetext,rubymine > .gitignore

gemのインストール

cp /path/to/Gemfile ./ #TODO
bundle install

bootstrap の適用

rails g layout:install bootstrap3

font-awesome の使用

vi app/assets/stylesheets/framework_and_overrides.css.scss
 @import "bootstrap-sprockets";
 @import "bootstrap";
+@import "font-awesome";

simple_form

rails g simple_form:install --bootstrap

devise

rails g devise:install
rails g devise user
rake db:migrate

devise の日本語対応

rails g devise:views:locale ja
rails g devise:views:bootstrap_templates

日本語化

curl -L https://raw.github.com/svenfuchs/rails-i18n/master/rails/locale/ja.yml > config/locales/ja.yml

アプリケーション設定

vi config/application.rb
config.time_zone = 'Tokyo'
config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
config.i18n.default_locale = :ja
	
config.generators do |g|
  g.javascripts false
  g.stylesheets false
  g.helper false
  g.test_framework :rspec,
  fixture: true,
  view_specs: false,
  helper_specs: false,
  routing_specs: false,
  controller_specs: true,
  request_specs: true
  g.fixture_replacement :factory_girl, dir: 'spec/factories'
end

devise 用のリンクを app/views/layouts/_navigation_links.html.haml に追加

- if user_signed_in?
  %li= link_to current_user.email, root_path, :class => 'navbar-link'
  %li= link_to 'Edit profile', edit_user_registration_path, :class => 'navbar-link'
  %li= link_to "Logout", destroy_user_session_path, method: :delete, :class => 'navbar-link'
- else
  %li= link_to "Sign up", new_user_registration_path, :class => 'navbar-link'
  %li= link_to "Login", new_user_session_path, :class => 'navbar-link'

kaminari

rails g kaminari:config
rails g kaminari:views  bootstrap3

rspec

rails g rspec:install

テストの設定

spec/rails_helper.rb

RSpec.configure do |config|
  config.before :suite do
    DatabaseRewinder.clean_all
  end

  config.after :each do
    DatabaseRewinder.clean
  end
  
  config.before :all do
    FactoryGirl.reload
  end
end

spec/spec_helper.rb

  require 'factory_girl_rails'
  config.include FactoryGirl::Syntax::Methods

spec/factories/users.rb

actoryGirl.define do
  factory :user do
    sequence(:email) { |i| "user#{i}@example.com" }
    sequence(:password) { |i| "password#{i}" }
  end

end

.pryrc の作成

begin
  require 'hirb'
rescue LoadError
  # Missing goodies, bummer
end

if defined? Hirb
  # Slightly dirty hack to fully support in-session Hirb.disable/enable toggling
  Hirb::View.instance_eval do
    def enable_output_method
      @output_method = true
      @old_print = Pry.config.print
      Pry.config.print = proc do |*args|
        Hirb::View.view_or_page_output(args[1]) || @old_print.call(*args)
      end
    end

    def disable_output_method
      Pry.config.print = @old_print
      @output_method = nil
    end
  end

  Hirb.enable
end

erb2haml

rake haml:replace_erbs

確認用ページ作成

rails g controller welcome index

figaro

bundel exec figaro install
vi config/application.yml
figaro heroku:set -e production