Explorar el Código

Implemento webauthn

Gabriel Badano hace 2 años
padre
commit
76c5c2f411

+ 34 - 20
assets/css/login.css

@@ -4,11 +4,20 @@
     padding: 0;
 }
 
+body {
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    height: 70vh;
+}
+
 .login-wrap {
-    margin: 50px auto 0;
+    margin: 0 auto;
     max-width: 350px;
     padding: 15px;
     width: 100%;
+    position: relative;
+    height: 300px;
 }
 
 .logo {
@@ -21,34 +30,18 @@
     padding-top: 20px;
 }
 
-input[type="text"],
-input[type="password"],
 button {
     width: 100%;
     height: 40px;
-    -moz-outline-style: none;
-}
-
-input[type="text"],
-input[type="password"] {
-    border: 1px solid #bbb;
-    padding: 0 0 0 10px;
-    font-size: 14px;
-}
-
-input[type="text"]:focus,
-input[type="password"]:focus {
-    border: 1px solid #a13bb6;
 }
 
 a {
-    text-align: center;
-    font-size: 10px;
+    font-size: 14px;
     color: #a13bb6;
 }
 
-p {
-    padding-bottom: 10px;
+a span {
+    color: unset !important;
 }
 
 button:hover {
@@ -58,3 +51,24 @@ button:hover {
 button:active {
     box-shadow: 1px 1px 7px #222;
 }
+
+.acciones button {
+    margin: 15px 0 0 !important;
+}
+
+.form-group {
+    margin: 0;
+}
+
+.hello {
+    text-align: center;
+    margin: 15px 0 0;
+}
+
+.step {
+    height: 200px;
+}
+
+label {
+    display: none;
+}

+ 58 - 11
assets/css/startmin.css

@@ -14,15 +14,6 @@ body {
     background-color: #fff;
 }
 
-@media(min-width:768px) {
-    #page-wrapper {
-        position: inherit;
-        margin-left: 250px;
-        padding: 50px 15px 15px;
-        border-left: 1px solid #e7e7e7;
-    }
-}
-
 .navbar {
     min-height: 50px;
 }
@@ -181,6 +172,14 @@ body {
 }
 
 @media(min-width:768px) {
+
+    #page-wrapper {
+        position: inherit;
+        margin-left: 250px;
+        padding: 50px 15px 15px;
+        border-left: 1px solid #e7e7e7;
+    }
+
     .sidebar {
         z-index: 1;
         position: absolute;
@@ -193,6 +192,7 @@ body {
     .navbar-top-links .dropdown-alerts {
         margin-left: auto;
     }
+
 }
 
 .btn-outline {
@@ -722,6 +722,21 @@ table.dataTable thead .sorting:after {
     border-width: 1px;
 }
 
+.acciones > * {
+    margin-left: 0;
+    margin-right: 10px;
+}
+
+.acciones.text-right > * {
+    margin-left: 10px;
+    margin-right: 0;
+}
+
+.acciones.text-center > * {
+    margin-left: 10px;
+    margin-right: 10px;
+}
+
 .alert {
     margin-top: 20px;
 }
@@ -769,11 +784,21 @@ select:focus {
 }
 
 .navbar-inverse .navbar-toggle {
-    margin: 8px 15px 8px 0;
+    border: none;
+    margin: 10px 5px 0 0;
 }
 
 .navbar-inverse .navbar-toggle .icon-bar {
-    background-color: #666;
+    background-color: #d6d6d6;
+    height: 3px;
+}
+
+.navbar-inverse .navbar-toggle:focus, .navbar-inverse .navbar-toggle:hover {
+    background: none;
+}
+
+.navbar-inverse .navbar-toggle:focus span, .navbar-inverse .navbar-toggle:hover span {
+    background-color: #fff;
 }
 
 .contracted .sidebar {
@@ -843,6 +868,28 @@ select:focus {
         display: inline;
     }
 
+    .btn-toolbar {
+        text-align: left;
+        padding: 0;
+        margin-bottom: 10px;
+    }
+
+    .user-menu .user-name {
+        display: none;
+    }
+
+    .user-menu .dropdown-toggle {
+        padding: 15px 0 0;
+    }
+
+    .user-menu .dropdown-toggle i {
+        font-size: 23px;
+    }
+
+    .user-menu .dropdown-toggle .caret {
+        display: none;
+    }
+
 }
 
 .modal-dialog {

+ 5 - 0
assets/images/ajax-loader.svg

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.0" width="146px" height="40px" viewBox="0 0 128 35" xml:space="preserve">
+<g><circle fill="#0b62a4" cx="17.5" cy="17.5" r="17.5"/><animate attributeName="opacity" dur="600ms" begin="0s" repeatCount="indefinite" keyTimes="0;0.167;0.5;0.668;1" values="0.3;1;1;0.3;0.3"/></g>
+<g><circle fill="#0b62a4" cx="110.5" cy="17.5" r="17.5"/><animate attributeName="opacity" dur="600ms" begin="0s" repeatCount="indefinite" keyTimes="0;0.334;0.5;0.835;1" values="0.3;0.3;1;1;0.3"/></g>
+<g><circle fill="#0b62a4" cx="64" cy="17.5" r="17.5"/><animate attributeName="opacity" dur="600ms" begin="0s" repeatCount="indefinite" keyTimes="0;0.167;0.334;0.668;0.835;1" values="0.3;0.3;1;1;0.3;0.3"/></g>
+</svg>

BIN
assets/images/ajaxLoader.gif


+ 28 - 22
assets/js/startmin.js

@@ -2,13 +2,13 @@ $(function() {
 
     $('#side-menu').metisMenu();
 
-    $("#contract").click(function(e){
+    $("#contract").click(function(e) {
         e.preventDefault();
         var w = $("#wrapper");
         if (w.hasClass("contracted")) {
             w.removeClass("contracted");
             setCookie('sidebar-contracted', '0');
-        }else {
+        } else {
             w.addClass("contracted");
             setCookie('sidebar-contracted', '1');
         }
@@ -31,37 +31,43 @@ $(function() {
             arr.splice(0);
             path = '/' + arr.join('/') + (arr.length > 0 ? '/' : '');
         }
-        var d           = new Date();
+        var d = new Date();
         d.setTime(d.getTime() + (365 * 24 * 60 * 60 * 1000));
         document.cookie = name + "=" + value + "; expires=" + d.toUTCString() + "; path=/";
     }
 
+    if ($.blockUI) {
+        $.blockUI.defaults.message = '<img src="assets/images/ajax-loader.svg" alt="">';
+        $.blockUI.defaults.css = {
+            padding  : 0,
+            margin   : '0 auto',
+            width    : '30%',
+            top      : '40%',
+            left     : '35%',
+            textAlign: 'center',
+            color    : '#000'
+        };
+        $.blockUI.defaults.overlayCSS = {
+            backgroundColor: '#000',
+            opacity        : 0.8
+        }
+    }
 });
 
 //Loads the correct sidebar on window load,
 //collapses the sidebar on window resize.
 // Sets the min-height of #page-wrapper to window size
 $(function() {
-    // $(window).bind("load resize", function() {
-    //     topOffset = 50;
-    //     width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
-    //     if (width < 768) {
-    //         $('div.navbar-collapse').addClass('collapse');
-    //         topOffset = 100; // 2-row-menu
-    //     } else {
-    //         $('div.navbar-collapse').removeClass('collapse');
-    //     }
-    //
-    //     height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1;
-    //     height = height - topOffset;
-    //     if (height < 1) height = 1;
-    //     if (height > topOffset) {
-    //         $("#page-wrapper").css("min-height", (height) + "px");
-    //     }
-    // });
+    $(window).bind("load resize", function() {
+        let width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
+        if (width < 768)
+            $('div.navbar-collapse').addClass('collapse');
+        else
+            $('div.navbar-collapse').removeClass('collapse');
+    });
 
-    var url = window.location;
-    var element = $('ul.nav a').filter(function() {
+    const url = window.location;
+    const element = $('ul.nav a').filter(function() {
         return this.href == url /*|| url.href.indexOf(this.href) == 0*/;
     }).addClass('active').parent().parent().addClass('in').parent();
     if (element.is('li')) {

+ 2 - 2
composer.json

@@ -20,7 +20,7 @@
     "vendor-dir": "protected/vendor"
   },
   "require": {
-    "oxusmedia/webapp": ">=2.0.0",
+    "oxusmedia/webapp": ">=2.4.2",
     "ext-json": "*"
   },
   "repositories": [
@@ -37,4 +37,4 @@
       "php protected/vendor/oxusmedia/webapp/scripts/installassets.php assets"
     ]
   }
-}
+}

+ 20 - 0
db.sql

@@ -48,6 +48,26 @@ INSERT INTO `usuarios` VALUES (1,'admin','c8837b23ff8aaa8a2dde915473ce0991','Adm
 /*!40000 ALTER TABLE `usuarios` ENABLE KEYS */;
 UNLOCK TABLES;
 
+create table usuarios_credenciales
+(
+    id bigint auto_increment primary key,
+    usuario_id    bigint       null,
+    credential_id varchar(255) not null,
+    public_key    text         null,
+    aaguid        varchar(255) null,
+    dispositivo   varchar(50)  null,
+    registrado    datetime     null,
+    ultimo_login  datetime     null,
+    data          text         null,
+    constraint aaguid
+        unique (aaguid, usuario_id),
+    constraint credential_id
+        unique (credential_id)
+);
+
+create index usuario_id
+    on usuarios_credenciales (usuario_id);
+
 /*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
 /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
 /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;

+ 15 - 13
protected/config/config.php

@@ -2,23 +2,25 @@
 
 return array(
 
-    'SITE'          => 'http://localhost/webapp-skeleton/',
-    'TITULO'        => 'SitioAdmin',
+    'SITE'   => 'http://localhost/webapp-skeleton/',
+    'TITULO' => 'SitioAdmin',
 
-    'DEBUG'         => true,
+    'LOGIN_WITH_EMAIL' => true,
 
-    'DB_SERVER'     => 'localhost',
-    'DB_USER'       => 'root',
-    'DB_PASS'       => '',
-    'DB_DB'         => 'webapp',
+    'DEBUG' => true,
+
+    'DB_SERVER' => 'localhost',
+    'DB_USER'   => 'root',
+    'DB_PASS'   => '',
+    'DB_DB'     => 'webapp',
 
     'DIR_PROTECTED' => $_SERVER['DOCUMENT_ROOT'] . '/webapp-skeleton/protected/',
 
-//    'MAINTENANCE_MODE_ENABLED'    => true,
-//    'MAINTENANCE_MODE_PASS'       => '12345',
-//    'MAINTENANCE_MODE_NOT_FOUND'  => true,
-//    'MAINTENANCE_MODE_BACKGROUND' => '#333',
-//    'MAINTENANCE_MODE_COLOR'      => '#CCC',
-//    'MAINTENANCE_MODE_LOGO'       => '/assets/images/logo.jpg',
+    //    'MAINTENANCE_MODE_ENABLED'    => true,
+    //    'MAINTENANCE_MODE_PASS'       => '12345',
+    //    'MAINTENANCE_MODE_NOT_FOUND'  => true,
+    //    'MAINTENANCE_MODE_BACKGROUND' => '#333',
+    //    'MAINTENANCE_MODE_COLOR'      => '#CCC',
+    //    'MAINTENANCE_MODE_LOGO'       => '/assets/images/logo.jpg',
 
 );

+ 149 - 24
protected/controllers/site.php

@@ -1,6 +1,7 @@
 <?php
 
 use oxusmedia\webApp\controller;
+use oxusmedia\webApp\MyWebauthn;
 use oxusmedia\webApp\form;
 use oxusmedia\webApp\column;
 use oxusmedia\webApp\input;
@@ -9,11 +10,14 @@ use oxusmedia\webApp\button;
 
 class site extends controller
 {
+    const LOGIN_METHOD_PASSWORD = 'P';
+    const LOGIN_METHOD_WEBAUTHN = 'W';
+
     public function index()
     {
         $this->webApp()->requireLoginRedir();
 
-        $this->titulo = '';
+        $this->titulo = 'Bienvenido';
 
         $this->render('index');
     }
@@ -24,11 +28,51 @@ class site extends controller
 
             $this->redirect($this->webApp()->getSite());
 
-        else{
+        else {
 
-            if (isset($_POST["login"])) {
+            $form = new form('login', array(
+                new column(array(
+                    ($this->webApp()->getConfig('LOGIN_WITH_EMAIL')
+                        ? new input('email', array(
+                            'rules'       => array(
+                                'required' => true,
+                                'email'    => true
+                            ),
+                            'htmlOptions' => array(
+                                'placeholder' => 'Email',
+                                'class'       => 'step1'
+                            ),
+                            'value' => $_COOKIE['login_email'] ?? ''
+                        ))
+                        : new input('usuario', array(
+                            'rules'       => array(
+                                'required' => true
+                            ),
+                            'htmlOptions' => array(
+                                'placeholder' => 'Usuario',
+                                'class'       => 'step1'
+                            )
+                        ))
+                    ),
+                    new password('pass', array(
+                        'htmlOptions' => array(
+                            'placeholder' => 'Contraseña',
+                            'class'       => 'step2'
+                        )
+                    ))
+                ))
+            ), array(
+                'buttons' => array(
+                    new button('siguiente', button::SUBMIT, button::PRIMARY),
+                    new button('cancelar', button::BUTTON, button::SECONDARY, array(
+                        'htmlOptions' => array(
+                            'class' => 'step2'
+                        )
+                    ))
+                )
+            ));
 
-                $form = $this->constructLoginForm();
+            if (isset($_POST['login'])) {
 
                 $form->setAtributes($_POST['login']);
 
@@ -36,9 +80,12 @@ class site extends controller
 
                     $param = $form->getAtributes();
 
-                    if ($this->webApp()->login($param["usuario"], $param["contrasena"])) {
+                    if ($this->webApp()->login($param['email'], $param['pass'])) {
+
+                        $this->webApp()->setCookie('login_email', $param['email']);
+                        $this->webApp()->setCookie('login_method', $this::LOGIN_METHOD_PASSWORD);
 
-                        $this->redirect($this->webApp()->getSite());
+                        $this->redirect($this->getMethodUrl('registerWebauthn'));
 
                         return;
 
@@ -50,42 +97,120 @@ class site extends controller
 
             $this->titulo = 'Iniciar sesión';
 
-            $this->addCss($this->webApp()->getSite() . 'assets/css/login.css');
+            MyWebauthn::initialize();
+
+            $this->addCss($this->webApp()->getUrlAssets() . 'css/login.css');
+
+            $this->addJs($this->webApp()->getUrlAssets() . 'webapp/js/jquery.validate.min.js');
+            $this->addJs($this->webApp()->getUrlAssets() . 'webapp/js/additional-methods.min.js');
+            $this->addJs($this->webApp()->getUrlAssets() . 'webapp/js/jquery.validate.messages_es_AR.js');
 
             $this->render('login', array(
-                'loginForm' => $this->constructLoginForm()
+                'method' => $_COOKIE['login_method'] ?? $this::LOGIN_METHOD_PASSWORD,
+                'form'   => $form
             ));
 
         }
 
     }
 
-    private function constructLoginForm()
+    public function loginWebauthnStep1()
     {
-        return new form('login',
-            array(
-                new column(array(
-                    new input('usuario'),
-                    new password('contrasena', array(
-                        'label' => 'Contraseña',
-                    ))
+        if (isset($_GET['email']))
+            $this->returnJson(
+                MyWebauthn::loginStep1($_GET['email'])
+            );
+    }
+
+    public function loginWebauthnStep2()
+    {
+        $post = trim(file_get_contents('php://input'));
+
+        if ($post and isset($_GET['email'])) {
+
+            $return = MyWebauthn::loginStep2($post, $_GET['email']);
+
+            if ($return->success) {
+                $this->webApp()->setCookie('login_email', $_GET['email']);
+                $this->webApp()->setCookie('login_method', $this::LOGIN_METHOD_WEBAUTHN);
+            }
+
+            $this->returnJson($return);
+
+        }
+
+    }
+
+    public function registerWebauthn()
+    {
+        $this->titulo = 'Bienvenido';
+
+        MyWebauthn::initialize();
+
+        $this->addJs($this->webApp()->getUrlAssets() . 'webapp/js/jquery.validate.min.js');
+        $this->addJs($this->webApp()->getUrlAssets() . 'webapp/js/additional-methods.min.js');
+        $this->addJs($this->webApp()->getUrlAssets() . 'webapp/js/jquery.validate.messages_es_AR.js');
+
+        $form = new form('register', array(
+            new column(array(
+                new input('dispositivo', array(
+                    'label' => '',
+                    'htmlOptions' => array(
+                        'placeholder' => 'Dispositivo'
+                    )
                 ))
-            ),
-            array(
-                'buttons' => array(
-                    new button('login', button::SUBMIT, button::PRIMARY, array(
-                        'label' => 'Iniciar sesión'
-                    ))
-                )
+            ))
+        ), array(
+            'buttons' => array(
+                new button('siguiente', button::SUBMIT, button::PRIMARY),
+                new button('cancelar', button::BUTTON, button::SECONDARY)
             )
+        ));
+
+        $this->render('webauthn', array(
+            'form' => $form
+        ));
+    }
+
+    public function registerWebauthnStep1()
+    {
+        $this->webApp()->requireLogin();
+
+        $this->returnJson(
+            MyWebauthn::registerStep1($this->webApp()->getUsuarioId())
         );
     }
 
+    public function registerWebauthnStep2()
+    {
+        $this->webApp()->requireLogin();
+
+        $post = trim(file_get_contents('php://input'));
+
+        if ($post) {
+
+            $return = MyWebauthn::registerStep2($post, $_GET['device']);
+
+            if ($return->success)
+                $this->webApp()->setCookie('login_method', $this::LOGIN_METHOD_WEBAUTHN);
+
+            $this->returnJson($return);
+
+        }
+    }
+
     public function logout()
     {
         $this->webApp()->logout();
 
-        $this->redirect($this->webApp()->getSite() . 'site/login');
+        $this->redirect($this->getMethodUrl('login'));
+    }
+
+    public function downloadCertificates()
+    {
+        $return = MyWebauthn::downloadFidoCertificates();
+
+        $this->returnJson($return);
     }
 
 }

+ 95 - 42
protected/controllers/usuario.php

@@ -27,7 +27,7 @@ class usuario extends controller
         ));
     }
 
-    private function configGrid()
+    private function configGrid() : grid
     {
         $grid = new grid('usuarios');
 
@@ -36,17 +36,12 @@ class usuario extends controller
             ->setUniqueIdFields('id')
             ->setColModel(array(
                 array(
-                    'name'   => 'usuario',
-                    'width'  => 150,
-                    'format' => grid::FMT_STRING
-                ),
-                array(
-                    'name'   => 'nombre',
+                    'name'   => $this->webApp()->getConfig('LOGIN_WITH_EMAIL') ? 'email' : 'usuario',
                     'width'  => 200,
                     'format' => grid::FMT_STRING
                 ),
                 array(
-                    'name'   => 'email',
+                    'name'   => 'nombre',
                     'width'  => 200,
                     'format' => grid::FMT_STRING
                 ),
@@ -65,9 +60,9 @@ class usuario extends controller
             ->setDefaultSortName('usuario')
             ->setDefaultSortOrder('asc')
             ->setActions(array(
-                new gridActionButton(gridActionButton::ADD, $this->webApp()->getSite() . 'usuario/add'),
-                new gridActionButton(gridActionButton::EDIT, $this->webApp()->getSite() . 'usuario/edit'),
-                new gridActionButton(gridActionButton::MULTI_DELETE, $this->webApp()->getSite() . 'usuario/delete')
+                new gridActionButton(gridActionButton::ADD, $this->getMethodUrl('add')),
+                new gridActionButton(gridActionButton::EDIT, $this->getMethodUrl('edit')),
+                new gridActionButton(gridActionButton::MULTI_DELETE, $this->getMethodUrl('delete'))
             ));
 
         return $grid;
@@ -97,18 +92,19 @@ class usuario extends controller
 
             new column(array(
 
-                new input('usuario', array(
-                    'rules' => array(
-                        'required' => true
-                    )
-                )),
-
-                new input('email', array(
-                    'rules' => array(
-                        'required' => true,
-                        'email'    => true
-                    )
-                )),
+                ($this->webApp()->getConfig('LOGIN_WITH_EMAIL')
+                    ? new input('email', array(
+                        'rules' => array(
+                            'required' => true,
+                            'email'    => true
+                        )
+                    ))
+                    : new input('usuario', array(
+                        'rules' => array(
+                            'required' => true
+                        )
+                    ))
+                ),
 
                 new password('pass', array(
                     'label' => 'Contraseña',
@@ -128,7 +124,7 @@ class usuario extends controller
             ))
 
         ), array(
-            'action' => $this->webApp()->getSite() . 'usuario/add',
+            'action' => $this->getMethodUrl('add'),
             'ajax'   => true,
             'gridId' => "usuarios"
         ));
@@ -175,12 +171,19 @@ class usuario extends controller
 
                     new hidden('id'),
 
-                    new input('email', array(
-                        'rules' => array(
-                            'required' => true,
-                            'email'    => true
-                        )
-                    )),
+                    ($this->webApp()->getConfig('LOGIN_WITH_EMAIL')
+                        ? new input('email', array(
+                            'rules' => array(
+                                'required' => true,
+                                'email'    => true
+                            )
+                        ))
+                        : new input('usuario', array(
+                            'rules' => array(
+                                'required' => true
+                            )
+                        ))
+                    ),
 
                     new password('pass', array(
                         'label'       => 'Contraseña',
@@ -200,7 +203,7 @@ class usuario extends controller
                 ))
 
             ), array(
-                'action' => $this->webApp()->getSite() . 'usuario/edit',
+                'action' => $this->getMethodUrl('edit'),
                 'ajax'   => true,
                 'gridId' => "usuarios"
             ));
@@ -230,11 +233,14 @@ class usuario extends controller
 
                 }
 
-            }else{
+            } else {
 
                 $form->setAtributes($usuario);
 
-                echo $form->render();
+                $this->render('edit', array(
+                    'form'         => $form,
+                    'dispositivos' => $this->getDispositivos($usuario->id)
+                ));
 
             }
 
@@ -250,12 +256,17 @@ class usuario extends controller
 
             $db = $this->db();
 
-            $usuario = $db->queryRow('SELECT * FROM usuarios WHERE id IN(:ids) AND usuario = "admin"', array(
-                'ids' => implode(',', $_POST['id'])
+            $usuario = $db->queryRow('SELECT * FROM usuarios WHERE id IN(:ids) AND usuario = :admin', array(
+                'ids'   => implode(',', $_POST['id']),
+                'admin' => "admin"
             ));
 
             if (!$usuario) {
 
+                $db->query('DELETE FROM usuarios_credenciales WHERE usuario_id IN(:ids)', array(
+                    'ids' => implode(',', $_POST['id'])
+                ));
+
                 $db->query('DELETE FROM usuarios WHERE id IN(:ids)', array(
                     'ids' => implode(',', $_POST['id'])
                 ));
@@ -264,7 +275,7 @@ class usuario extends controller
                     'error' => 0
                 ));
 
-            }else{
+            } else {
 
                 $this->returnJson(array(
                     'error'   => 1,
@@ -296,12 +307,19 @@ class usuario extends controller
 
             new column(array(
 
-                new input('email', array(
-                    'rules' => array(
-                        'required' => true,
-                        'email'    => true
-                    )
-                )),
+                ($this->webApp()->getConfig('LOGIN_WITH_EMAIL')
+                    ? new input('email', array(
+                        'rules' => array(
+                            'required' => true,
+                            'email'    => true
+                        )
+                    ))
+                    : new input('usuario', array(
+                        'rules' => array(
+                            'required' => true
+                        )
+                    ))
+                ),
 
                 new password('pass', array(
                     'label'       => 'Contraseña',
@@ -367,6 +385,41 @@ class usuario extends controller
         ));
     }
 
+    public function dispositivos()
+    {
+        $this->webApp()->requireLoginRedir();
+
+        $this->titulo = 'Mis dispositivos';
+
+        if (isset($_GET['id'])) {
+
+            $this->db()->query('DELETE FROM usuarios_credenciales WHERE id = :id AND usuario_id = :usuario_id', array(
+                'id'         => $_GET['id'],
+                'usuario_id' => $this->webApp()->getUsuarioId()
+            ));
+
+            $this->notify('Dispositivo eliminado correctamente', notificacion::SUCCESS);
+
+            $this->redirect($this->getMethodUrl('dispositivos'));
+
+        } else {
+
+            $this->addJs($this->webApp()->getUrlAssets() . 'webapp/js/jquery.blockUI.js');
+
+            $this->render("dispositivos", array(
+                'dispositivos' => $this->getDispositivos($this->webApp()->getUsuarioId())
+            ));
+
+        }
+    }
+
+    private function getDispositivos($id)
+    {
+        return $this->db()->query('SELECT * FROM usuarios_credenciales WHERE usuario_id = :id', array(
+            'id' => $id
+        ));
+    }
+
     public function theme()
     {
         $this->webApp()->requireLoginRedir();

+ 2 - 4
protected/views/_includes/header.php

@@ -21,7 +21,7 @@
 
                 <div class="row">
 
-                    <div class="col-xs-6">
+                    <div class="col-sm-4">
 
                 <?php } ?>
 
@@ -31,7 +31,7 @@
 
                     </div>
 
-                    <div class="col-xs-6">
+                    <div class="col-sm-8">
 
                         <nav class="btn-toolbar"><?php echo $nav;?></nav>
 
@@ -41,6 +41,4 @@
 
                 <?php } ?>
 
-                <hr class="page-title-hr">
-
                 <?php $this->renderInclude("notifications");?>

+ 4 - 3
protected/views/_includes/menu.php

@@ -17,12 +17,13 @@
 
     <!-- Top Navigation: Right Menu -->
     <ul class="nav navbar-right navbar-top-links">
-        <li class="dropdown">
+        <li class="dropdown user-menu">
             <a class="dropdown-toggle" data-toggle="dropdown" href="#">
-                <i class="fa fa-user fa-fw"></i> <?php echo $this->webApp()->getUsuario();?> <b class="caret"></b>
+                <i class="fa fa-user fa-fw"></i> <span class="user-name"><?php echo $this->webApp()->getUsuarioNombre();?></span> <b class="caret"></b>
             </a>
             <ul class="dropdown-menu dropdown-user">
                 <li><a href="usuario/miperfil"><i class="fa fa-user fa-fw"></i> Mi perfil</a></li>
+                <li><a href="usuario/dispositivos"><i class="fa fa-laptop fa-fw"></i> Mis dispositivos</a></li>
                 <li class="divider"></li>
                 <li><a href="site/logout"><i class="fa fa-sign-out fa-fw"></i> Cerrar sesión</a></li>
             </ul>
@@ -31,7 +32,7 @@
 
     <!-- Sidebar -->
     <div class="navbar-default sidebar" role="navigation">
-        <div class="sidebar-nav navbar-collapse collapse">
+        <div class="sidebar-nav navbar-collapse">
 
             <ul class="nav" id="side-menu">
 

+ 105 - 6
protected/views/site/login.php

@@ -1,21 +1,120 @@
 <!DOCTYPE html>
-<html>
+<html lang="es">
 <head>
 
-    <title><?php echo $this->titulo . ' | ' . $this->webApp()->getConfig('TITULO');?></title>
+    <title><?php echo $this->titulo . ' | ' . $this->webApp()->getConfig('TITULO'); ?></title>
 
-    <?php $this->renderInclude("head");?>
+    <?php $this->renderInclude("head"); ?>
+
+    <script>
+
+        $(document).ready(function(){
+
+            let step;
+            step1();
+
+            $('#cancelar').click(function(e){
+                step1();
+                e.preventDefault();
+            });
+
+            $('#login').submit(function(e){
+                if ($(this).valid()) {
+                    if (step === 1) {
+                        <?php if ($method == $this::LOGIN_METHOD_WEBAUTHN) { ?>
+                            $('#hello').click();
+                        <?php } else { ?>
+                            step2();
+                        <?php } ?>
+                        e.preventDefault();
+                    }
+                }
+            });
+
+            $('#hello').click(function(e) {
+                webauthn_login($('#email').val(), function() {
+                    step2();
+                });
+                e.preventDefault();
+            });
+
+            function step1()
+            {
+                $('.step1').show();
+                $('.step2').hide();
+                $('#siguiente').html('Siguiente');
+                $('#email').focus();
+                step = 1;
+            }
+
+            function step2()
+            {
+                $('.step1').hide();
+                $('.step2').show();
+                $('#siguiente').html('Inciar sesión');
+                if (is_webauthn_supported()) {
+                    $('#os').html(webauthn_os());
+                } else {
+                    $('.hello').hide();
+                }
+                $('#pass').focus();
+                step = 2;
+            }
+
+        });
+
+    </script>
 
 </head>
 <body>
 
     <div class="login-wrap">
 
-        <h2 class="logo"><?php echo $this->webApp()->getConfig('TITULO');?></h2>
+        <h2 class="logo">Iniciar sesión</h2>
+
+        <?php echo $form->render();?>
+
+        <p class="hello step2">
+            <a href="#" id="hello">Iniciar sesión con <span id="os"></span></a>
+        </p>
+
+        <?php /*
+
+        <form role="form" id="login" method="post">
+
+            <div class="step step1">
+                <div class="form-group">
+                    <input type="email" name="email" id="email" value="<?php echo $email;?>" class="form-control" placeholder="Email" required>
+                </div>
+                <div class="acciones">
+                    <p>
+                        <button type="submit" class="btn btn-primary">Siguiente</button>
+                    </p>
+                </div>
+            </div>
+
+            <div class="step step2">
+                <div class="form-group">
+                    <input type="password" name="pass" id="pass" value="" class="form-control" placeholder="Contraseña">
+                </div>
+                <div class="acciones">
+                    <p>
+                        <button type="submit" id="submit" class="btn btn-primary">Iniciar Sesión</button>
+                    </p>
+                    <p>
+                        <button type="button" id="cancel" class="btn btn-secondary">Cancelar</button>
+                    </p>
+                    <p class="hello">
+                        <a href="#" id="hello">Iniciar sesión con <span id="os"></span></a>
+                    </p>
+                </div>
+            </div>
+
+        </form>
 
-        <?php echo $loginForm->render();?>
+        */ ?>
 
     </div>
 
 </body>
-</html>
+</html>

+ 78 - 0
protected/views/site/webauthn.php

@@ -0,0 +1,78 @@
+<?php $this->renderInclude("header");?>
+
+    <hr class="page-title-hr">
+
+    <div class="step step1">
+
+        <h3>¿Desea habilitar el inicio de sesión con <span id="os"></span> en este dispositivo?</h3>
+
+        <div class="acciones">
+            <button type="button" class="btn btn-primary" id="enable">Habilitar</button>
+            <button type="button" class="btn btn-secondary" id="cancel">Cancelar</button>
+        </div>
+
+    </div>
+
+    <div class="step step2">
+
+        <h3>Ingrese un nombre para identificar este dispositivo</h3>
+
+        <?php echo $form->render();?>
+
+    </div>
+
+    <script>
+
+        $(document).ready(function(){
+
+            if (is_webauthn_supported()) {
+
+                $('#os').html(webauthn_os());
+
+                let step;
+                step1();
+
+                $('#enable').click(function(e) {
+                    step2();
+                    e.preventDefault();
+                });
+
+                $('#register').submit(function(e) {
+                    if ($(this).valid())
+                        webauthn_register($('#dispositivo').val());
+                    e.preventDefault();
+                });
+
+                $('#cancel').click(function(e) {
+                    window.location = '';
+                    e.preventDefault();
+                });
+
+                $('#cancelar').click(function(e) {
+                    window.location = '';
+                    e.preventDefault();
+                });
+
+                function step1() {
+                    $('.step1').show();
+                    $('.step2').hide();
+                    step = 1;
+                }
+
+                function step2() {
+                    $('.step1').hide();
+                    $('.step2').show();
+                    step = 2;
+                }
+
+            } else {
+
+                window.location = '';
+
+            }
+
+        });
+
+    </script>
+
+<?php $this->renderInclude("footer");?>

+ 50 - 0
protected/views/usuario/dispositivos.php

@@ -0,0 +1,50 @@
+
+<?php $this->renderInclude("header");?>
+
+    <hr class=".page-title-hr">
+
+    <table class="table table-hover table-striped">
+        <thead>
+            <tr>
+                <th>Dispositivo</th>
+                <th>Registrado</th>
+                <th>Último inicio de sesión</th>
+                <th></th>
+            </tr>
+        </thead>
+        <tbody>
+            <?php while ($d = $this->db()->getRow($dispositivos)) { ?>
+                <tr>
+                    <td><?php echo $d->dispositivo;?></td>
+                    <td><?php echo $d->registrado;?></td>
+                    <td><?php echo $d->ultimo_login;?></td>
+                    <td><button type="button" class="btn btn-danger btn-xs delete" data-id="<?php echo $d->id;?>">Eliminar</button></td>
+                </tr>
+            <?php } ?>
+        </tbody>
+    </table>
+
+    <script>
+
+        $(document).ready(function(){
+            $('.delete').click(function(){
+                const id = $(this).attr('data-id');
+                $.webApp_modal('¿Desea eliminar el dispositivo?', 'Eliminar', [
+                    {
+                        text: 'Eliminar',
+                        eventFunction: function(){
+                            $.blockUI();
+                            window.location = 'usuario/dispositivos?id=' + id;
+                        }
+                    },
+                    {
+                        text: 'Cancelar',
+                        close: true
+                    }
+                ], 'S');
+            });
+        });
+
+    </script>
+
+<?php $this->renderInclude("footer");?>

+ 29 - 0
protected/views/usuario/edit.php

@@ -0,0 +1,29 @@
+
+<?php echo $form->render();?>
+
+<?php if ($this->db()->numRows($dispositivos) > 0) { ?>
+
+    <hr>
+
+    <h3>Dispositivos</h3>
+
+    <table class="table table-hover table-striped">
+        <thead>
+        <tr>
+            <th>Dispositivo</th>
+            <th>Registrado</th>
+            <th>Último inicio de sesión</th>
+        </tr>
+        </thead>
+        <tbody>
+        <?php while ($d = $this->db()->getRow($dispositivos)) { ?>
+            <tr>
+                <td><?php echo $d->dispositivo;?></td>
+                <td><?php echo $d->registrado;?></td>
+                <td><?php echo $d->ultimo_login;?></td>
+            </tr>
+        <?php } ?>
+        </tbody>
+    </table>
+
+<?php } ?>

+ 2 - 0
protected/views/usuario/miperfil.php

@@ -1,6 +1,8 @@
 
 <?php $this->renderInclude("header");?>
 
+    <hr class=".page-title-hr">
+
     <?php echo $form->render();?>
 
 <?php $this->renderInclude("footer");?>