【ionic2】ログイン機能を実装する

はじめに

今回やりたかったのは、まぁ普通のログイン機能をもつアプリ。実装したい要件としては以下
・未ログインの場合は、ログイン画面を表示。
・ログイン済の場合は、他の画面が見れる。

実装方法としては以下のような形で行ってみた。
・ログイン済みかどうかはローカルストレージに保存して判定する。
・app.component.ts の platform.ready()でローカルストレージをチェック。
・ログイン済みの場合は他の画面を表示
・未ログインの場合はログイン画面を表示。ログインに成功したらローカルストレージに書き込んでindex.htmlをリロードする。リロードすることでplatform.ready()が再度呼ばれるので次はログインと判定されるはず。

実装

src/app/app.component.ts

・・・
import { Storage } from '@ionic/storage';
・・・
export class MyApp {

  rootPage:any;

  constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, storage: Storage) {
    platform.ready().then(() => {

      statusBar.styleDefault();
      splashScreen.hide();

      storage.get('auth').then((val) => {
        if (val == null) {
          // 未ログインの場合はログイン画面を表示
          this.rootPage = 'signin';
        } else {
          // ログイン済みの場合はその他の画面を表示
          this.rootPage = 'home';
	}
      });
    });
  }
}

src/pages/signin/signin.ts

import { Storage } from '@ionic/storage';

・・・

export class SigninPage {

  ・・・

  // ログイン
  signin() {
    this.ApiService.get('/signin', {})
      .then(data => {
        // ログインに成功したらログイン情報をローカルストレージに保存して、リロード
        this.storage.set('auth', { token:data.token });                                                                                                                                                            
        location.reload();                                                                                                                                                                                         
      })
      .catch(messages => {
	alert(messages);
      });
  }

  /* ログアウトも手抜きここで書いちゃう */
  signout() {
    // ローカルストレージを削除してリロード
    this.storage.remove('auth');
    location.reload();
  }
}

なんか正当な方法じゃなさそうだけど、自分がやってるアプリだとこんなので十分事足りそう。以上です。

【ionic2】apiリクエストをproviderで共通処理化する

はじめに

今回やりたかったことは以下
APIのURLを開発と本番で切り替えられるようにしたい
・アプリ全体で認証トークンをhttpヘッダーに含めたい

これらを満たすAPIリクエストの共通処理実装したのでメモしておく。

APIのURLを開発と本番で切り替える

こちらのURLのとおりにやったらできた。
http://roblouie.com/article/296/ionic-2-environment-variables-the-best-way/

webpackの設定とかはURLのとおりにやればできる。あとは以下のように各々呼び出せるようになる。

import { Injectable, Inject } from '@angular/core';
import { EnvVariables } from '../app/environment-variables/environment-variables.token';

@Injectable()
export class ApiService {

  constructor(http: Http, @Inject(EnvVariables) public envVariables) {
  }

  test() {
    // this.envVariables.apiEndpoint
  }
}

今回は上の記事に書いてあるそのまんまつかわせてもらった。

src/app/environment-variables/development.ts

export const devVariables = {
  apiEndpoint: 'http://dev.example.com',
}

src/app/environment-variables/production.ts

export const prodVariables = {
  apiEndpoint: 'http://prod.example.com',
}
APIリクエストを共通化する

Angularではproviderとして作るのが一般的ぽいのでそのようにしてみる。

ひな形作成

$ ionic g provider api-service

実行すると src/providers/api-service.ts が生成されるのでここに実装する。
今回は認証用のトークンをローカルストレージに保存しておいて、リクエストのたびにhttpヘッダーにさしこむようにした。

src/providers/api-service.ts

import { Injectable, Inject } from '@angular/core';
import { Http, Headers } from '@angular/http';
import 'rxjs/add/operator/map';

import { Storage } from '@ionic/storage';
import { EnvVariables } from '../app/environment-variables/environment-variables.token';

@Injectable()
export class ApiService {

  constructor(public http: Http, @Inject(EnvVariables) public envVariables,
              private storage: Storage) {
    /* コンストラクタ */
  }

  request(method, action, params) {
    return new Promise((resolve, reject) => {
      this.storage.get('auth.token')
        .then(token => {
          var url = this.envVariables.apiEndpoint + action;
          var http = null;
          var headers = { headers: new Headers({ 'Authorization': 'Token ' + token }) };
          if (method == 'get') {
            http = this.http.get(url, headers)
          } else if (method == 'post') {
            http = this.http.post(url, data, headers)
          }
          if (http !== null) {
            http.subscribe((response) => {
              if (response.status == 200) {
                var result = response.json();
                if (result.status == "success") {
                  resolve(result);
                } else {
                  reject(result.messages);
                }
              } else {
                reject('通信エラーが発生しました');
              }
            },err => {
              reject(err);
            });
          }
        });
    });
  }

  get(url, params) {
    return this.request('get', url, params);
  }

  post(url, data) {
    return this.request('post', url, params);
  }
}

これでコンポーネントから呼び出せる
src/pages/hello.js

import { ApiService } from '../../providers/api-service';

・・・

@Component({
  selector: 'page-hello',
  templateUrl: 'hello.html',
  providers: [ApiService]
})
export class SamplePage {
  constructor(public navCtrl: NavController, private ApiService: ApiService) {
  }

  test() {
    this.ApiService.get('/users/', {})
      .then(data => {
        // success
        console.log(data);
      })
      .catch(messages => {
        // error
        alert(messages);
      });
  }
}

以上です。

【rails】authenticate_with_http_token を使ってapiの認証機能を実装する

はじめに

今回やりたかったことはアプリからのリクエストを受け付けるapiの開発。やりたいことと流れは以下のようなイメージ

・Emailとパスワードでログインを行う
・ログインに成功したらトークンと有効期限を発行してクライアントに返す
・クライアントは毎リクエストークンをhttpヘッダーに入れてリクエストする

また、今回はrails5のapiモードを使用しました。

テーブル設計とか準備

・users

idユーザーid
emailメール
password_digestパスワード
tokenトーク
token_expireトークン有効期限

password_digestはrailsのhas_secure_passwordというのを使う場合にこのカラムが必要なようなので追加。has_secure_passwordはパスワード認証の仕組みを簡単に実装できる仕組みみたい。

とりあえずモデルとテーブルを作成する

$ ./bin/rails g model User

migrationファイルが生成されるのでカラムを追記する
db/migrate/20171009182615_create_users.rb

class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users do |t|

      t.string :email
      t.string :password_digest
      t.string :token
      t.datetime :token_expire

      t.timestamps
    end
  end
end

modelファイルも生成されるのでバリデーションを追加
app/models/user.rb

class User < ApplicationRecord

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }

  has_secure_password
  validates :password, presence: true, length: { minimum: 6 }

end

has_secure_passwordを使うためにbcryptというのが必要なのでGemfileに追記
Gemfile

gem 'bcrypt', '~> 3.1.7'

ここまでできたら以下を実行

$ ./bin/bundle install
$ ./bin/rails db:migrate

実装

あとはプログラムを実装していく。

config/routes.rb

Rails.application.routes.draw do
  post '/users/signup', to:'users#signup'
  post '/users/signin', to:'users#signin'
end

app/controller/users_controller.rb

# coding: utf-8                                                                                                                                                                                                    
class UsersController < ApplicationController

  ## authenticate_with_http_tokenを使うために
  include ActionController::HttpAuthentication::Token::ControllerMethods

  ## signinとsingup以外で認証処理を行う
  before_action :auth, except: [:signin, :signup]

  ## サインアップ
  def signup
    user = User.new(user_params)
    if user.save
      success
    else
      failed(user.errors.full_messages)
    end
  end

  ## サインイン
  def signin                                                                                                                                                                                                       
    ## 認証に成功したらランダム文字列でトークンを生成して有効期限1日をセット
    user = User.find_by(email: params[:email])
    if user && user.authenticate(params[:password])
      user.token = create_token
      user.token_expire = DateTime.now + 1
      if user.save!(:validate => false)
        success({ token: user.token })
      else
        error
      end
    else
      error('email または passwordが正しくありません')
    end
  end                                                                                                                                                                                                              

  ## レスポンス返却用の簡易メソッド                                                                                                                                                                                
  def success(data = nil)
    render json:{ status: 'success', result: data }
  end

  def error(messages = nil)
    render json:{ status: 'failed', messages: messages }
  end

  ## 以下 authenticate_with_http_token を使った処理 
  def auth                                                                                                                                                                                                         
    ## 認証に失敗したらエラーレスポンスを返す
    authenticate_token || render_unauthorized
  end

  ## httpヘッダーに入っているtokenでdbを検索して有効期限のチェック
  ## authenticate_with_http_tokenの書き方でこのように書くみたい
  def authenticate_token
    authenticate_with_http_token do |token, options|
      user = User.find_by(token: token)
      user && DateTime.now <= user.token_expire
    end
  end

  def render_unauthorized
    render json:{ result:'failed' }
  end

  ## ランダム文字列を作る
  def create_token
    return ((0..9).to_a + ("a".."z").to_a + ("A".."Z").to_a).sample(50).join
  end

  def user_params
    params.permit(:email, :password)
  end

end


確認はこんな感じで

$ curl -H 'Authorization: Token xxxxxxxxxxxxxxxxxxxxxxxxxx' http://localhost:3000/users


今回はトークンをDBに入れているけど、少し規模の大きいサービスだったらredisとかにもたせるのがよいのかな。
以上です。

rails console で group by の結果を確認

やりかったことはemailが重複しているレコードの件数をしらべたかっただけ。 

実行されているSQLが期待通りだったのでこれでいいのかと思ってたけど、全然期待した結果にならない。。

$ User.select('email,count(*)').group('email')
> User.select('email,count(*)').group('email')
User Load (0.3ms)  SELECT email,count(*) FROM "users" GROUP BY "users"."email"
[#<User:0x007fc90f100ce0 id: nil, email: "test@test.com">,
#<User:0x007fc90f0f80b8 id: nil, email: "test2@test.com">,
・・・

結果、こうするらしい

$ User.group(:email).count
> User.group(:email).count
   (0.3ms)  SELECT COUNT(*) AS count_all, "users"."email" AS users_email FROM "users" GROUP BY "users"."email"
{"test@test.com"=>1,
 "test2@test.com"=>1,
 ・・・

慣れかな。以上です

【ionic2】ionicPageで各画面をurlに対応させる

はじめに

ionic1のルーティングまわりはui-routerで検索すれば色々出て来たけどionic2で何で検索すればよいのかまだよくわかってないけどとりあえずやり方はわかったのでメモしておく。デバッグのときとかはブラウザでリロードしたいし。あれ、けど目的はそれくらい。。?

とりあえずHome → Page1 → Page2 みたいな感じで画面遷移できるように。
それぞれ /#/home → /#/page1 → /#/page2 とURLもつようしてみる。 

app/app.component.ts

・・・
import { HomePage } from '../pages/home/home';
@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  //rootPage:any = HomePage;                                                                                                                                                                                       
  rootPage:any = 'home'; // ★ ココをあとでionicPageで指定する名前に変更                                                                                                                                           
                                                                                                                                                                                                                   
  ・・・
}

app/app.module.ts

・・・
@NgModule({
  declarations: [
    MyApp
    //HomePage                                                                                                                                                                                                     
  ],

  ・・・

  entryComponents: [
    MyApp
    //HomePage                                                                                                                                                                                                     
  ],

  ・・・

以降、各ページ

pages/home/home.module.ts

import { NgModule } from '@angular/core';
import { IonicPageModule } from 'ionic-angular';

import { HomePage } from '../home/home';

@NgModule({
  declarations: [
    HomePage
  ],
  imports: [
    IonicPageModule.forChild(HomePage)
  ],
  entryComponents: [
    HomePage
  ]
})
export class HomePageModule {}

pages/home/home.ts

import { Component } from '@angular/core';
import { NavController, IonicPage } from 'ionic-angular';

import { Page1Page } from '../page1/page1';

@IonicPage({
  name: 'home'
})
@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  constructor(public navCtrl: NavController) {
  }

  goPage1() {
    this.navCtrl.push('page1');
  }
}

page1、page2も同じように作ればNavControllerにpushするとurlも書き換わることが確認できた。以上です。

【ionic2】画面遷移のアニメーションを変更する

やりたかったこととしては画面の遷移の際のアニメーションでandroidでもiosみたいに左から右にスライドするようにしたいとういこと。
本来はプラットフォームごとに奨励されているアニメーションを使うべきだとは思うのだがやっぱりiosをベースで考えている人だとandroidでもそのように実装してほしいというのはよくあるのかと。

アプリ全体で設定する場合
app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { ErrorHandler, NgModule } from '@angular/core';

・・・

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    SignupPage
  ],
  imports: [
    BrowserModule,
    // ★ ここで設定
    IonicModule.forRoot(MyApp, {
      pageTransition: 'ios-transition'                                                                                                                                                                             
    })
  ],

  ・・・

})
export class AppModule {}

画面ごとに設定したい場合
各pagesのコンポーネント

this.navCtrl.push(MyPage, null, {
  animation: 'ios-transition'
});

app.module.ts で設定するやり方は他にも色々設定できるみたいなので覚えた方がよさそう。
http://ionicframework.com/docs/api/config/Config/

以上です

【ionic2】無限スクロールの使い方

なかなか手をだせにずにいたionic2を次の案件では使えるように準備中。とりあえずionic1でできてたことをionic2でもできるようになることが当面の目標。今回はapiから取得したjsonからリスト表示で、無限スクロールを実装してみる。

blankでプロジェクト作成

$ ionic start test blank

まずはhttpをつかえるように。

app/app.module.ts

import { BrowserModule } from '@angular/platform-browser';

・・・

import { HttpModule } from '@angular/http'; ## ★ これを追加

@NgModule({

    ・・・ 

    imports: [
        BrowserModule,
        IonicModule.forRoot(MyApp),
        HttpModule ## ★ これを追加
    ],

    ・・・ 

})
export class AppModule {}

つづいてコントローラ?を実装

src/pages/home/home.ts

import { Component } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
c})
export class HomePage {

  page = 1;
  items = [];
  completed = false;

  constructor(public http: Http) {
    this.getUsers().subscribe(data => {
      if (0 < data.users.length) {
        this.items = this.items.concat(data.users);
        this.page++;
      } else {
        this.completed = true;
      }
    });
  }

  doInfinite(infiniteScroll) {
    this.getUsers().subscribe(data => {
      if (0 < data.users.length) {
        this.items = this.items.concat(data.users);
        this.page++;
      } else {
        this.completed = true;
      }
      infiniteScroll.complete();
    });
  }

  getUsers() {
    return this.http
      .get('http://localhost:3000/users?page=' + this.page)
      .map(res => res.json());
  }

}

さいごにテンプレートを実装

src/pages/home/home.html

・・・

<ion-content padding>
  <ion-list>
    <ion-item *ngFor="let item of items">
      {{ item.name }}
    </ion-item>
  </ion-list>
  <ion-infinite-scroll (ionInfinite)="doInfinite($event)" *ngIf="completed">
    <ion-infinite-scroll-content></ion-infinite-scroll-content>
  </ion-infinite-scroll>
</ion-content>

typescriptとかrxjsとかまだ全然わかってないけどとりあえずこれでできた。以上です。