AngularJS

【AngularJS/Symfony2.3】AngularJS用のXSRFトークン発行/チェックサービス。

前回のエントリで若干触れたのでペタリ。

<?php

namespace Hoge\FugaBundle\Services;

use Symfony\Component\HttpFoundation\Cookie;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\SessionCsrfProvider;

class XsrfTokenManager
{
    private $request;
    
    private $provider;
    
    protected $_cookieName = 'XSRF-TOKEN';
    
    protected $_sessionName = 'S-XSRF-TOKEN';
    
    const RECEIVED_HEADER_NAME = 'X-XSRF-TOKEN';
    
    /**
     * Construct
     */
    public function __construct(Request $request, SessionCsrfProvider $provider)
    {
        $this->request = $request;
        $this->provider = $provider;
    }
    
    /**
     * check xsrf token matches
     * 
     * @return boolean
     */
    public function check()
    {
        $_header = $this->request->headers->get(self::RECEIVED_HEADER_NAME);
        
        if (empty($_header) || $_header != $this->create()) {
            
            throw new \Symfony\Component\Security\Core\Exception\AccessDeniedException();
        }
        
        return true;
    }
    
    /**
     * generate strings of xsrf token
     * 
     * @param string $salt
     * @return \Hoge\FugaBundle\Services\XsrfTokenManager
     */
    private function create($salt = 'csrf_token')
    {
        $request = $this->request;
        
        $token = null;
        
        if ($request->getSession() && $request->getSession()->has($this->_sessionName)) {
            $token = $request->getSession()->get($this->_sessionName);
        } else {
            $token =  $this->provider->generateCsrfToken($salt);
            if ($request->getSession()) {
                $request->getSession()->set($this->_sessionName, $token);
            }
        }
        
        return $token;
    }

    /**
     * set cookie for client
     * 
     * @return boolean|\Symfony\Component\HttpFoundation\Cookie
     */
    public function set()
    {
        $cookie = new Cookie($this->_cookieName, $this->create(), 0, '/', null, false, false);
        
        $response = new Response();
        $response->headers->setCookie($cookie);
        $response->send();
        
        return $cookie;
    }
    
    /**
     * setter for cookie name
     * 
     * @param unknown $name
     * @return \Hoge\FugaBundle\Services\XsrfTokenManager
     */
    public function setCookieName($name)
    {
        $this->_cookieName = $name;
        
        return $this;
    }
    
    /**
     * getter for cookie name
     * 
     * @return string
     */
    public function getCookieName()
    {
        return $this->_cookieName;
    }
    
    /**
     * setter for session name
     * 
     * @param unknown $name
     * @return \Hoge\FugaBundle\Services\XsrfTokenManager
     */
    public function setSessionName($name)
    {
        $this->_sessionName = $name;
        
        return $this;
    }
    
    /**
     * getter for session name
     * 
     * @return string
     */
    public function getSessionName()
    {
        return $this->_sessionName;
    }
    
}

これをService.ymlで下記のように定義してやり
※前回のエントリそのまんま。

services:
    xsrf.token.manager:
        class: Hoge\FugaBundle\Services\XsrfTokenManager
        arguments: [@request, @form.csrf_provider]
        scope: request
        tags:
            - { name: xsrf.token.manager }

レスポンスリスナあたりでset()メソッドを実行してやればOK。

services:
    listener.before.filter:
        class: Hoge\FugaBundle\EventListener\ResponseListener
        arguments: [@xsrf.token.manager]
        scope: request
        tags:
            - { name: kernel.event_listener, event: kernel.response, method: createToken }

ResponseListenerは下記の通り。

\Hoge\FugaBundle\EventListener\ResponseListener.php

<?php
namespace Hoge\FugaBundle\EventListener;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Hoge\FugaBundle\Services\XsrfTokenManager;

class ResponseListener
{
    
    protected $manager;
    
    /**
     * Construct
     */
    public function __construct(XsrfTokenManager $manager)
    {
        $this->manager = $manager;
    }
    
    /**
     * before filter event
     */
    public function createToken()
    {
        $this->manager->set();
    }
    
}

あとはトークンチェックが必要なコントローラーのアクション先頭でcheck()メソッドを呼んでやればOK。
※本来はここもリスナーでやったほうがなおベター。

 

【AngularJS】のfilter群を超強力に拡張してくれるangular-filter。

かなり便利なライブラリを見つけたのでメモ。

https://github.com/a8m/angular-filter

上記モジュールを読み込むと便利なフィルター群が色々と使えるようになる。

詳細は上記URLのDocumentを参照されたし。

 

【AngularJS】リッチなアラートUIを提供してくれる、ngSweetAlert。

ANGULAR MODULESを巡回してた時に見つけたのでペタリ。

http://ngmodules.org/modules/ngSweetAlert

シンプルだけどかなりリッチでかっこいいアラートアニメーションを実装することが出来る。

今度使ってみよう。

 

【AngularJS】textarea内のカーソル位置を習得する。

便利なディレクティブをみつけたのでペタリ。

https://github.com/eirikb/angular-caret

 

【AngularJS】で空の配列を判定したい場合。

例えばビューのng-showとかng-hideとかで。

<div ng-hide="!!array.length">配列が空の場合現れる</div>
<div ng-show="!!!array.length">配列が空の場合現れる</div>

配列のlengthプロパティを参照してやればOK。

 

【AngularJS】Angular-UIのパスワードバリデーションツール。

という神がかった仕組みをstackさんを巡回してた際に発見したのでメモ。

<input name="password" required ng-model="password">
<input name="confirm_password"
       ui-validate="'$value==password'"
       ui-validate-watch="'password'">

 Passwords match? {{!!form.confirm_password.$error.validator}}

これはマジで神。

 

【Symfony2.3/AngularJS】FOSRestBundleとFOSUserBundleで構築するRESTログインAPI。

説明が皆無なのでかなり不親切です。超絶自分用まとめ。もしくはある程度解る人用。

「/」以下をフロント、「/admin」以下を管理画面(要ログイン)とする、よくあるアプリケーションの構築テンプレート。

□□サーバーサイド

■SecurityController.php

namespace Hoge\FugaBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

use Hoge\FugaBundle\Entity\User;

/**
 * @Route("api")
 */
class SecurityController extends Controller
{
    protected function getUserManager()
    {
        return $this->get('fos_user.user_manager');
    }

    protected function loginUser(User $user)
    {
        $security = $this->get('security.context');
        $providerKey = $this->container->getParameter('fos_user.firewall_name');
        $roles = $user->getRoles();
        $token = new UsernamePasswordToken($user, null, $providerKey, $roles);
        $security->setToken($token);
    }

    protected function logoutUser()
    {
        $security = $this->get('security.context');
        $token = new AnonymousToken(null, new User());
        $security->setToken($token);
        $this->get('session')->invalidate();
    }
    
    protected function checkUser()
    {
        $security = $this->get('security.context');
        
        if ($token = $security->getToken()) {
            $user = $token->getUser();
            if ($user instanceof User) {
                return $user;
            }
        }
        
        return false;
    }

    protected function checkUserPassword(User $user, $password)
    {
        $factory = $this->get('security.encoder_factory');
        $encoder = $factory->getEncoder($user);
        if(!$encoder){
            return false;
        }
        return $encoder->isPasswordValid($user->getPassword(), $password, $user->getSalt());
    }

    /**
     * @Route("/login.{_format}", name="hoge_fuga_admin_api_security_login", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function loginAction()
    {
        $request = $this->getRequest();
        $username = $request->get('username');
        $password = $request->get('password');
        
        $um = $this->getUserManager();
        $user = $um->findUserByUsername($username);
        if(!$user){
            $user = $um->findUserByEmail($username);
        }
        
        if(!$user instanceof User){
            throw new NotFoundHttpException('ユーザーIDが存在しません');
        }
        if(!$this->checkUserPassword($user, $password)){
            throw new AccessDeniedException('パスワードが不正です');
        }
        
        $this->loginUser($user);
        return [
            'success' => true,
            'user' => $user
        ];
    }

    /**
     * @Route("/logout.{_format}", name="hoge_fuga_admin_api_security_logout", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function logoutAction()
    {
        $this->logoutUser();
        
        return [
            'success' => true
        ];
    }
    
    /**
     * @Route("/loginCheck.{_format}", name="hoge_fuga_admin_api_security_login_check", defaults={"_format" = "json"})
     * @Method("POST")
     */
    public function loginCheckAction()
    {
        if ($user = $this->checkUser()) {
            return [
                'success' => true,
                'user' => $user
            ];
        } else {
            throw new AccessDeniedException();
        }
    }
}

■app/config/config.yml

fos_user:
    db_driver: orm 
    firewall_name: admin_area
    user_class: Hoge\FugaBundle\Entity\User

fos_rest:
    view:
        view_response_listener: force
        force_redirects:
            html: true
        formats:
            jsonp: true
            json: true
            xml: true
            rss: false
        failed_validation: HTTP_BAD_REQUEST
        default_engine: twig

■app/config/security.yml

security:
    encoders:
        Symfony\Component\Security\Core\User\User: plaintext
        FOS\UserBundle\Model\UserInterface: sha512

    role_hierarchy:
        ROLE_ADMIN: ROLE_USER
        ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]

    providers:
        in_memory:
            memory:
                users:
                    user:  { password: userpass, roles: [ 'ROLE_USER' ] }
                    admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
        fos_userbundle:
            id: fos_user.user_provider.username_email

    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

        admin_area:
            pattern: ^/admin
            anonymous: ~
            form_login:
                login_path: /admin/login
                check_path: /admin/login_check

    access_control:
        - { path: ^/admin/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin/api/login.*$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/admin, roles: [ ROLE_ADMIN ] }

■app/config/routing.yml
※FOSUserBundleのインストールに必要

fos_user:
    resource: "@FOSUserBundle/Resources/config/routing/all.xml"

■src/Hoge/FugaBundle/Resources/config/routing.yml

hoge_fuga_admin_index:
    resource: "@HogeFugaBundle/Controller/DefaultController.php"
    type:     annotation
    prefix:   /admin

hoge_fuga_admin_security:
    resource: "@HogeFugaBundle/Controller/SecurityController.php"
    type:     annotation
    prefix:   /admin

■DefaultController.php

namespace Hoge\FugaBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

class DefaultController extends Controller
{
    
    /**
     * @Route("")
     * @Route("/{_all}", name="affiliate_cms_admin_index_all", requirements={"_all" = "^(?!.*(api)).*$"})
     * @Method("GET")
     */
    public function indexAction()
    {
        return $this->render('HogeFugaBundle:Default:index.html.twig');
    }
}

「api」を含まないURLを全てDefaultControllerのIndexActionで拾い、あとはAngularのルーターに任せる。

□□フロントサイド

■adminLoginController.js(ログイン画面で用いるコントローラー)

void function() {
    
    var adminLoginController = function(securityResource, $state, UserService) {
        
        var that = this;
        
        this.data = {};
        
        this.loginFailure = false;
        
        this.login = function() {
            securityResource.save({action: 'login.json'}, {
                username: that.data.username,
                password: that.data.password
            }, function(response) {
                UserService.setUser(response.user);
                $state.go('admin.main.dashboard');
            }, function(response) {
                if (response.status >= 400) {
                    that.loginFailure = true;
                }
            });
        }
    }

    angular
        .module('hogeControllers')
        .controller('adminLoginController', ['securityResource', '$state', 'UserService', adminLoginController])
    ;
    
}();

■adminController.js(ログイン画面を含むログイン後の全てのコントローラーが継承している基底コントローラー)

void function() {
    
    var adminController = function(securityResource, $state, UserService) {

        this.user = null;
        
        var that = this;
        
        this.logout = function() {
            securityResource.save({action: 'logout.json'}, {}, function() {
                UserService.clear();
                $state.go('admin.login');
            });
        }
        
        this.initUser = function() {
            that.user = UserService.getUser();
            if (!that.user) {
                securityResource.save({action: 'loginCheck.json'}, {}, function(response) {
                    that.user = response.user;
                }, function() {
                    UserService.clear();
                    $state.go('admin.login');
                });
            }
        }
    }
    
    angular
        .module('hogeControllers')
        .controller('adminController', ['securityResource', '$state', 'UserService', adminController])
    ;
    
}();

■SecurityResource.js

void function() {
    
    angular
        .module('hogeResources')
        .factory('securityResource', ['$resource', function($resource) {
            return $resource('/admin/api/:action', {
                action: '@action',
            }, {
            }
        )}])
        ;
    
}();

■UserService.js

void function() {
    
    var UserService = function() {
        
        this.user = null;
        
        this.getUser = function() {
            return this.user;
        }
        
        this.setUser = function(user) {
            this.user = user;
        }
        
        this.clear = function() {
            this.user = null;
        }
        
    }
    
    angular
        .module('hogeServices')
        .service('UserService', [UserService])
    ;
    
}();

■app.js

void function() {
    
    angular.module('hogeApp', [
        // コア
        'ngAnimate',
        'ngResource',
        'ngSanitize',
        'ngTouch',
        
        // 必要に応じて
        'ui',
        'ui.utils',
        'ui.router',
        'ui.bootstrap',
        'ui.bootstrap.datetimepicker',
        'kendo.directives',
        'ngCookies',
        'angular-loading-bar',
        'angularFileUpload',
        'cgNotify',
        
        // 自分のやつら
        'hogeConstants',
        'hogeControllers',
        'hogeDirectives',
        'hogeFactories',
        'hogeFilters',
        'hogeResources',
        'hogeServices'
    ]);
    angular.module('hogeConstants', []);
    angular.module('hogeControllers', []);
    angular.module('hogeDirectives', []);
    angular.module('hogeFactories', []);
    angular.module('hogeFilters', []);
    angular.module('hogeResources', []);
    angular.module('hogeServices', []);
    
    /**
     * config section
     */
    angular
        .module('hogeApp')
        .config(['$urlRouterProvider', '$stateProvider', '$locationProvider',
            function($urlRouterProvider, $stateProvider, $locationProvider) {
            $stateProvider
                
                /**
                 * admin
                 */
                .state('admin', {
                    url: '/admin',
                    views: {
                        main: {
                            templateUrl: '/admin.html',
                            controller: 'adminController',
                            controllerAs: 'admin'
                        }
                    }
                })
                
                /**
                 * admin login
                 */
                .state('admin.login', {
                    url: '/login',
                    views: {
                        main: {
                            templateUrl: '/login.html',
                            controller: 'adminLoginController',
                            controllerAs: 'adminLogin'
                        }
                    }
                })
                
                /**
                 * admin main (base template)
                 */
                .state('admin.main', {
                    url: '/',
                    views: {
                        main: {
                            templateUrl: '/main.html',
                            controller: 'adminMainController',
                            controllerAs: 'adminMain'
                        }
                    }
                })
                
                /**
                 * admin main dashboard
                 */
                .state('admin.main.dashboard', {
                    url: 'dashboard',
                    views: {
                        main: {
                            templateUrl: '/dashboard/index.html',
                            controller: 'adminMainDashboardController',
                            controllerAs: 'adminMainDashboard'
                        }
                    }
                })
            ;
            
            $urlRouterProvider.otherwise('/admin/dashboard');
            
            $locationProvider.html5Mode(true).hashPrefix('!');
            
        }]);
    
}();

こんな感じでベースが完成。

めでたしめでたし。

 

【AngularJS】$animateでshakeアニメーションを実装する。

Macのログイン失敗したときのあの動きね。

まずはCSSに下記を定義。
※数値は好みで変更。

@-webkit-keyframes shake {
    0% {transform: translateX(0);}
    12.5% {transform: translateX(-12px) rotateY(-10deg)}
    37.5% {transform: translateX(10px) rotateY(8deg)}
    62.5% {transform: translateX(-6px) rotateY(-4deg)}
    87.5% {transform: translateX(4px) rotateY(2deg)}
    100% {transform: translateX(0)}
}

@keyframes shake {
    0% {transform: translateX(0);}
    12.5% {transform: translateX(-12px) rotateY(-10deg)}
    37.5% {transform: translateX(10px) rotateY(8deg)}
    62.5% {transform: translateX(-6px) rotateY(-4deg)}
    87.5% {transform: translateX(4px) rotateY(2deg)}
    100% {transform: translateX(0)}
}

.shake {
  animation: shake 800ms ease-in-out;
  -webkit-animation: shake 800ms ease-in-out;
}

んでshakeしたいタイミングでこう。

$animate.addClass(element, 'shake', function() {
    $animate.removeClass(element, 'shake');
});

elementは揺らしたい対象要素。

ディレクティブに逃すと更にイイね。