Jose Cienfuegos 9 ヶ月 前
コミット
ad7ebed873
共有93 個のファイルを変更した4198 個の追加0 個の削除を含む
  1. 35 0
      .gitignore
  2. 29 0
      LICENSE.md
  3. 1 0
      README.md
  4. 92 0
      Vagrantfile
  5. 31 0
      assets/AppAsset.php
  6. 27 0
      codeception.yml
  7. 24 0
      commands/BdController.php
  8. 83 0
      composer.json
  9. 33 0
      config/__autocomplete.php
  10. 48 0
      config/console.php
  11. 12 0
      config/db.php
  12. 5 0
      config/params.php
  13. 42 0
      config/test.php
  14. 6 0
      config/test_db.php
  15. 88 0
      config/web.php
  16. 128 0
      controllers/SiteController.php
  17. 9 0
      docker-compose.yml
  18. 22 0
      mail/layouts/html.php
  19. 58 0
      migrations/m220310_234138_inicio.php
  20. 75 0
      models/Media.php
  21. 26 0
      models/ModeloBase.php
  22. 88 0
      models/Usuario.php
  23. 38 0
      modules/common/Module.php
  24. 159 0
      modules/common/data/Respuesta.php
  25. 72 0
      modules/common/models/Usuario.php
  26. 32 0
      modules/common/rest/AuthController.php
  27. 84 0
      modules/common/rest/JsonController.php
  28. 19 0
      modules/common/rest/Serializer.php
  29. 18 0
      modules/common/rest/UrlRule.php
  30. 38 0
      modules/pdf/Module.php
  31. 502 0
      modules/pdf/web/Controller.php
  32. 38 0
      modules/v1/Module.php
  33. 20 0
      modules/v1/controllers/DefaultController.php
  34. 52 0
      modules/v1/controllers/IniciarSesionController.php
  35. 82 0
      modules/v1/controllers/MediaController.php
  36. 19 0
      modules/v1/controllers/PerfilController.php
  37. 88 0
      modules/v1/controllers/SubirArchivoController.php
  38. 179 0
      modules/v1/controllers/UsuarioController.php
  39. 34 0
      modules/v1/models/Media.php
  40. 19 0
      modules/v1/models/Sesion.php
  41. 33 0
      modules/v1/models/Usuario.php
  42. 162 0
      requirements.php
  43. 2 0
      runtime/.gitignore
  44. 6 0
      tests/_bootstrap.php
  45. 1 0
      tests/_data/.gitkeep
  46. 2 0
      tests/_output/.gitignore
  47. 26 0
      tests/_support/AcceptanceTester.php
  48. 23 0
      tests/_support/FunctionalTester.php
  49. 26 0
      tests/_support/UnitTester.php
  50. 10 0
      tests/acceptance.suite.yml.example
  51. 12 0
      tests/acceptance/AboutCest.php
  52. 34 0
      tests/acceptance/ContactCest.php
  53. 18 0
      tests/acceptance/HomeCest.php
  54. 21 0
      tests/acceptance/LoginCest.php
  55. 1 0
      tests/acceptance/_bootstrap.php
  56. 29 0
      tests/bin/yii
  57. 20 0
      tests/bin/yii.bat
  58. 13 0
      tests/functional.suite.yml
  59. 57 0
      tests/functional/ContactFormCest.php
  60. 59 0
      tests/functional/LoginFormCest.php
  61. 1 0
      tests/functional/_bootstrap.php
  62. 11 0
      tests/unit.suite.yml
  63. 3 0
      tests/unit/_bootstrap.php
  64. 41 0
      tests/unit/models/ContactFormTest.php
  65. 51 0
      tests/unit/models/LoginFormTest.php
  66. 44 0
      tests/unit/models/UserTest.php
  67. 261 0
      tests/unit/widgets/AlertTest.php
  68. 2 0
      vagrant/config/.gitignore
  69. 22 0
      vagrant/config/vagrant-local.example.yml
  70. 38 0
      vagrant/nginx/app.conf
  71. 3 0
      vagrant/nginx/log/.gitignore
  72. 18 0
      vagrant/provision/always-as-root.sh
  73. 79 0
      vagrant/provision/once-as-root.sh
  74. 31 0
      vagrant/provision/once-as-vagrant.sh
  75. 50 0
      vagrant/provision/provision.awk
  76. 28 0
      views/layouts/main.php
  77. 18 0
      views/site/about.php
  78. 68 0
      views/site/contact.php
  79. 27 0
      views/site/error.php
  80. 53 0
      views/site/index.php
  81. 49 0
      views/site/login.php
  82. 4 0
      web/.htaccess
  83. 2 0
      web/assets/.gitignore
  84. 54 0
      web/css/pdf.css
  85. 84 0
      web/css/site.css
  86. BIN
      web/favicon.ico
  87. 16 0
      web/index-test.php
  88. 12 0
      web/index.php
  89. 2 0
      web/info.php
  90. 2 0
      web/robots.txt
  91. 73 0
      widgets/Alert.php
  92. 21 0
      yii
  93. 20 0
      yii.bat

+ 35 - 0
.gitignore

@@ -0,0 +1,35 @@
+# phpstorm project files
+.idea
+.vscode
+
+# netbeans project files
+nbproject
+
+# zend studio for eclipse project files
+.buildpath
+.project
+.settings
+
+# windows thumbnail cache
+Thumbs.db
+
+# composer vendor dir
+/vendor
+
+# composer itself is not needed
+composer.phar
+composer.lock
+
+# Mac DS_Store Files
+.DS_Store
+
+# phpunit itself is not needed
+phpunit.phar
+# local phpunit config
+/phpunit.xml
+
+tests/_output/*
+tests/_support/_generated
+
+#vagrant folder
+/.vagrant

+ 29 - 0
LICENSE.md

@@ -0,0 +1,29 @@
+Copyright © 2008 by Yii Software LLC (http://www.yiisoft.com)
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ * Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in
+   the documentation and/or other materials provided with the
+   distribution.
+ * Neither the name of Yii Software LLC nor the names of its
+   contributors may be used to endorse or promote products derived
+   from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.

+ 1 - 0
README.md

@@ -0,0 +1 @@
+### Aquí se puede guardar detalles del proyecto

+ 92 - 0
Vagrantfile

@@ -0,0 +1,92 @@
+require 'yaml'
+require 'fileutils'
+
+required_plugins_installed = nil
+required_plugins = %w( vagrant-hostmanager vagrant-vbguest )
+required_plugins.each do |plugin|
+  unless Vagrant.has_plugin? plugin
+    system "vagrant plugin install #{plugin}"
+    required_plugins_installed = true
+  end
+end
+
+# IF plugin[s] was just installed - restart required
+if required_plugins_installed
+  # Get CLI command[s] and call again
+  system 'vagrant' + ARGV.to_s.gsub(/\[\"|\", \"|\"\]/, ' ')
+  exit
+end
+
+domains = {
+  app: 'yii2basic.test'
+}
+
+vagrantfile_dir_path = File.dirname(__FILE__)
+
+config = {
+  local: vagrantfile_dir_path + '/vagrant/config/vagrant-local.yml',
+  example: vagrantfile_dir_path + '/vagrant/config/vagrant-local.example.yml'
+}
+
+# copy config from example if local config not exists
+FileUtils.cp config[:example], config[:local] unless File.exist?(config[:local])
+# read config
+options = YAML.load_file config[:local]
+
+# check github token
+if options['github_token'].nil? || options['github_token'].to_s.length != 40
+  puts "You must place REAL GitHub token into configuration:\n/yii2-app-basic/vagrant/config/vagrant-local.yml"
+  exit
+end
+
+# vagrant configurate
+Vagrant.configure(2) do |config|
+  # select the box
+  config.vm.box = 'bento/ubuntu-18.04'
+
+  # should we ask about box updates?
+  config.vm.box_check_update = options['box_check_update']
+
+  config.vm.provider 'virtualbox' do |vb|
+    # machine cpus count
+    vb.cpus = options['cpus']
+    # machine memory size
+    vb.memory = options['memory']
+    # machine name (for VirtualBox UI)
+    vb.name = options['machine_name']
+  end
+
+  # machine name (for vagrant console)
+  config.vm.define options['machine_name']
+
+  # machine name (for guest machine console)
+  config.vm.hostname = options['machine_name']
+
+  # network settings
+  config.vm.network 'private_network', ip: options['ip']
+
+  # sync: folder 'yii2-app-advanced' (host machine) -> folder '/app' (guest machine)
+  config.vm.synced_folder './', '/app', owner: 'vagrant', group: 'vagrant'
+
+  # disable folder '/vagrant' (guest machine)
+  config.vm.synced_folder '.', '/vagrant', disabled: true
+
+  # hosts settings (host machine)
+  config.vm.provision :hostmanager
+  config.hostmanager.enabled            = true
+  config.hostmanager.manage_host        = true
+  config.hostmanager.ignore_private_ip  = false
+  config.hostmanager.include_offline    = true
+  config.hostmanager.aliases            = domains.values
+
+  # quick fix for failed guest additions installations
+  # config.vbguest.auto_update = false
+
+  # provisioners
+  config.vm.provision 'shell', path: './vagrant/provision/once-as-root.sh', args: [options['timezone'], options['ip']]
+  config.vm.provision 'shell', path: './vagrant/provision/once-as-vagrant.sh', args: [options['github_token']], privileged: false
+  config.vm.provision 'shell', path: './vagrant/provision/always-as-root.sh', run: 'always'
+
+  # post-install message (vagrant console)
+  config.vm.post_up_message = "App URL: http://#{domains[:app]}"
+end

+ 31 - 0
assets/AppAsset.php

@@ -0,0 +1,31 @@
+<?php
+/**
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+namespace app\assets;
+
+use yii\web\AssetBundle;
+
+/**
+ * Main application asset bundle.
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @since 2.0
+ */
+class AppAsset extends AssetBundle
+{
+    public $basePath = '@webroot';
+    public $baseUrl = '@web';
+    public $css = [
+        'css/site.css',
+    ];
+    public $js = [
+    ];
+    public $depends = [
+        'yii\web\YiiAsset',
+        'yii\bootstrap4\BootstrapAsset',
+    ];
+}

+ 27 - 0
codeception.yml

@@ -0,0 +1,27 @@
+actor: Tester
+bootstrap: _bootstrap.php
+paths:
+    tests: tests
+    log: tests/_output
+    data: tests/_data
+    helpers: tests/_support
+settings:
+    memory_limit: 1024M
+    colors: true
+modules:
+    config:
+        Yii2:
+            configFile: 'config/test.php'
+
+# To enable code coverage:
+#coverage:
+#    #c3_url: http://localhost:8080/index-test.php/
+#    enabled: true
+#    #remote: true
+#    #remote_config: '../codeception.yml'
+#    whitelist:
+#        include:
+#            - models/*
+#            - controllers/*
+#            - commands/*
+#            - mail/*

+ 24 - 0
commands/BdController.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace app\commands;
+
+use yii\console\Controller;
+use yii\console\ExitCode;
+
+class BdController extends Controller {
+
+  public function actionInsertarUsuario() {
+    $usuario = new \app\models\Usuario();
+    $usuario->uuid();
+    $usuario->correo = "soporte@edesarrollos.com";
+    $usuario->agregarClave("Edes@rrollos2023");
+    $usuario->nombre = "Soporte Técnico";
+    $usuario->rol = "admin";
+    $usuario->telefono = "1234567890";
+    if(!$usuario->save()) {
+      $this->stdout(json_encode($usuario->getFirstErrors()));
+    }
+    return ExitCode::OK;
+  }
+
+}

+ 83 - 0
composer.json

@@ -0,0 +1,83 @@
+{
+    "name": "yiisoft/yii2-app-basic",
+    "description": "Yii 2 Basic Project Template",
+    "keywords": ["yii2", "framework", "basic", "project template"],
+    "homepage": "http://www.yiiframework.com/",
+    "type": "project",
+    "license": "BSD-3-Clause",
+    "support": {
+        "issues": "https://github.com/yiisoft/yii2/issues?state=open",
+        "forum": "http://www.yiiframework.com/forum/",
+        "wiki": "http://www.yiiframework.com/wiki/",
+        "irc": "irc://irc.freenode.net/yii",
+        "source": "https://github.com/yiisoft/yii2"
+    },
+    "minimum-stability": "stable",
+    "require": {
+        "php": ">=5.6.0",
+        "yiisoft/yii2": "~2.0.14",
+        "yiisoft/yii2-bootstrap4": "~2.0.0",
+        "yiisoft/yii2-swiftmailer": "~2.0.0 || ~2.1.0",
+        "firebase/php-jwt": "^5.4",
+        "ramsey/uuid": "^4.2"
+    },
+    "require-dev": {
+        "yiisoft/yii2-debug": "~2.1.0",
+        "yiisoft/yii2-gii": "~2.2.0",
+        "yiisoft/yii2-faker": "~2.0.0",
+        "codeception/codeception": "^4.0",
+        "codeception/verify": "~0.5.0 || ~1.1.0",
+        "codeception/specify": "~0.4.6",
+        "symfony/browser-kit": ">=2.7 <=4.2.4",
+        "codeception/module-filesystem": "^1.0.0",
+        "codeception/module-yii2": "^1.0.0",
+        "codeception/module-asserts": "^1.0.0"
+    },
+    "config": {
+        "process-timeout": 1800,
+        "fxp-asset": {
+            "enabled": false
+        },
+        "allow-plugins": {
+            "yiisoft/yii2-composer": true
+        }
+    },
+    "scripts": {
+        "post-install-cmd": [
+            "yii\\composer\\Installer::postInstall"
+        ],
+        "post-create-project-cmd": [
+            "yii\\composer\\Installer::postCreateProject",
+            "yii\\composer\\Installer::postInstall"
+        ]
+    },
+    "extra": {
+        "yii\\composer\\Installer::postCreateProject": {
+            "setPermission": [
+                {
+                    "runtime": "0777",
+                    "web/assets": "0777",
+                    "yii": "0755"
+                }
+            ]
+        },
+        "yii\\composer\\Installer::postInstall": {
+            "generateCookieValidationKey": [
+                "config/web.php"
+            ]
+        }
+    },
+    "repositories": [
+        {
+            "type": "composer",
+            "url": "https://asset-packagist.org"
+        }
+    ],
+    "autoload": {
+        "psr-4": {
+            "common\\": "modules/common",
+            "v1\\": "modules/v1",
+            "pdf\\": "modules/pdf"
+        }
+    }
+}

+ 33 - 0
config/__autocomplete.php

@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * This class only exists here for IDE (PHPStorm/Netbeans/...) autocompletion.
+ * This file is never included anywhere.
+ * Adjust this file to match classes configured in your application config, to enable IDE autocompletion for custom components.
+ * Example: A property phpdoc can be added in `__Application` class as `@property \vendor\package\Rollbar|__Rollbar $rollbar` and adding a class in this file
+ * ```php
+ * // @property of \vendor\package\Rollbar goes here
+ * class __Rollbar {
+ * }
+ * ```
+ */
+class Yii {
+    /**
+     * @var \yii\web\Application|\yii\console\Application|__Application
+     */
+    public static $app;
+}
+
+/**
+ * @property yii\rbac\DbManager $authManager 
+ * @property \yii\web\User|__WebUser $user
+ * 
+ */
+class __Application {
+}
+
+/**
+ * @property app\models\User $identity
+ */
+class __WebUser {
+}

+ 48 - 0
config/console.php

@@ -0,0 +1,48 @@
+<?php
+
+$params = require __DIR__ . '/params.php';
+$db = require __DIR__ . '/db.php';
+
+$config = [
+  'id' => 'basic-console',
+  'basePath' => dirname(__DIR__),
+  'bootstrap' => ['log'],
+  'controllerNamespace' => 'app\commands',
+  'aliases' => [
+    '@bower' => '@vendor/bower-asset',
+    '@npm'   => '@vendor/npm-asset',
+    '@tests' => '@app/tests',
+  ],
+  'components' => [
+    'cache' => [
+      'class' => 'yii\caching\FileCache',
+    ],
+    'log' => [
+      'targets' => [
+        [
+          'class' => 'yii\log\FileTarget',
+          'levels' => ['error', 'warning'],
+        ],
+      ],
+    ],
+    'db' => $db,
+  ],
+  'params' => $params,
+  /*
+  'controllerMap' => [
+      'fixture' => [ // Fixture generation command line.
+          'class' => 'yii\faker\FixtureController',
+      ],
+  ],
+  */
+];
+
+if (YII_ENV_DEV) {
+  // configuration adjustments for 'dev' environment
+  $config['bootstrap'][] = 'gii';
+  $config['modules']['gii'] = [
+    'class' => 'yii\gii\Module',
+  ];
+}
+
+return $config;

+ 12 - 0
config/db.php

@@ -0,0 +1,12 @@
+<?php
+
+return [
+    'class' => 'yii\db\Connection',
+    'dsn' => 'pgsql:host=localhost;port=5432;dbname=hacienda-notificacion',
+    'username' => 'hacienda-notificacion',
+    'password' => 'hacienda-notificacion',
+    'charset' => 'utf8',
+    'enableSchemaCache' => true,
+    'schemaCacheDuration' => 60,
+    'schemaCache' => 'cache'
+];

+ 5 - 0
config/params.php

@@ -0,0 +1,5 @@
+<?php
+
+return [
+  'jwt.key' => 'XQky31fheBkkB5T4cYnD',
+];

+ 42 - 0
config/test.php

@@ -0,0 +1,42 @@
+<?php
+$params = require __DIR__ . '/params.php';
+$db = require __DIR__ . '/test_db.php';
+
+/**
+ * Application configuration shared by all test types
+ */
+return [
+    'id' => 'basic-tests',
+    'basePath' => dirname(__DIR__),
+    'aliases' => [
+        '@bower' => '@vendor/bower-asset',
+        '@npm'   => '@vendor/npm-asset',
+    ],
+    'language' => 'en-US',
+    'components' => [
+        'db' => $db,
+        'mailer' => [
+            'useFileTransport' => true,
+        ],
+        'assetManager' => [
+            'basePath' => __DIR__ . '/../web/assets',
+        ],
+        'urlManager' => [
+            'showScriptName' => true,
+        ],
+        'user' => [
+            'identityClass' => 'app\models\User',
+        ],
+        'request' => [
+            'cookieValidationKey' => 'test',
+            'enableCsrfValidation' => false,
+            // but if you absolutely need it set cookie domain to localhost
+            /*
+            'csrfCookie' => [
+                'domain' => 'localhost',
+            ],
+            */
+        ],
+    ],
+    'params' => $params,
+];

+ 6 - 0
config/test_db.php

@@ -0,0 +1,6 @@
+<?php
+$db = require __DIR__ . '/db.php';
+// test database! Important not to run tests on production or development databases
+$db['dsn'] = 'mysql:host=localhost;dbname=yii2basic_test';
+
+return $db;

+ 88 - 0
config/web.php

@@ -0,0 +1,88 @@
+<?php
+
+$params = require __DIR__ . '/params.php';
+$db = require __DIR__ . '/db.php';
+
+$config = [
+  'id' => 'basic',
+  'basePath' => dirname(__DIR__),
+  'language' => 'es',
+  'bootstrap' => ['log'],
+  'aliases' => [
+    '@bower' => '@vendor/bower-asset',
+    '@npm'   => '@vendor/npm-asset',
+  ],
+  'components' => [
+    'request' => [
+      'cookieValidationKey' => 'MwDzyBzzBQMMA8d_tiCkX-h5oMhAg8jK',
+      'parsers' => [
+        'application/json' => 'yii\web\JsonParser',
+      ],
+    ],
+    'cache' => [
+      'class' => 'yii\caching\FileCache',
+    ],
+    'user' => [
+      'identityClass' => 'app\models\User',
+      'enableAutoLogin' => true,
+    ],
+    'errorHandler' => [
+      'errorAction' => 'site/error',
+    ],
+    'mailer' => [
+      'class' => 'yii\swiftmailer\Mailer',
+      // send all mails to a file by default. You have to set
+      // 'useFileTransport' to false and configure transport
+      // for the mailer to send real emails.
+      'useFileTransport' => true,
+    ],
+    'log' => [
+      'traceLevel' => YII_DEBUG ? 3 : 0,
+      'targets' => [
+        [
+          'class' => 'yii\log\FileTarget',
+          'levels' => ['error', 'warning'],
+        ],
+      ],
+    ],
+    'db' => $db,
+    'urlManager' => [
+      'enablePrettyUrl' => true,
+      'showScriptName' => false,
+      'rules' => [
+        [
+          'class' => 'common\rest\UrlRule',
+          'controller' => [
+            'v1/perfil',
+            'v1/usuario',
+            'v1/media',
+          ],
+        ]
+      ],
+    ],
+  ],
+  'params' => $params,
+  'modules' => [
+    'v1' => ['class' => 'v1\Module'],
+    'pdf' => ['class' => 'pdf\Module'],
+  ]
+];
+
+if (YII_ENV_DEV) {
+  // configuration adjustments for 'dev' environment
+  $config['bootstrap'][] = 'debug';
+  $config['modules']['debug'] = [
+    'class' => 'yii\debug\Module',
+    // uncomment the following to add your IP if you are not connecting from localhost.
+    //'allowedIPs' => ['127.0.0.1', '::1'],
+  ];
+
+  $config['bootstrap'][] = 'gii';
+  $config['modules']['gii'] = [
+    'class' => 'yii\gii\Module',
+    // uncomment the following to add your IP if you are not connecting from localhost.
+    //'allowedIPs' => ['127.0.0.1', '::1'],
+  ];
+}
+
+return $config;

+ 128 - 0
controllers/SiteController.php

@@ -0,0 +1,128 @@
+<?php
+
+namespace app\controllers;
+
+use Yii;
+use yii\filters\AccessControl;
+use yii\web\Controller;
+use yii\web\Response;
+use yii\filters\VerbFilter;
+use app\models\LoginForm;
+use app\models\ContactForm;
+
+class SiteController extends Controller
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function behaviors()
+    {
+        return [
+            'access' => [
+                'class' => AccessControl::className(),
+                'only' => ['logout'],
+                'rules' => [
+                    [
+                        'actions' => ['logout'],
+                        'allow' => true,
+                        'roles' => ['@'],
+                    ],
+                ],
+            ],
+            'verbs' => [
+                'class' => VerbFilter::className(),
+                'actions' => [
+                    'logout' => ['post'],
+                ],
+            ],
+        ];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function actions()
+    {
+        return [
+            'error' => [
+                'class' => 'yii\web\ErrorAction',
+            ],
+            'captcha' => [
+                'class' => 'yii\captcha\CaptchaAction',
+                'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
+            ],
+        ];
+    }
+
+    /**
+     * Displays homepage.
+     *
+     * @return string
+     */
+    public function actionIndex()
+    {
+        return $this->render('index');
+    }
+
+    /**
+     * Login action.
+     *
+     * @return Response|string
+     */
+    public function actionLogin()
+    {
+        if (!Yii::$app->user->isGuest) {
+            return $this->goHome();
+        }
+
+        $model = new LoginForm();
+        if ($model->load(Yii::$app->request->post()) && $model->login()) {
+            return $this->goBack();
+        }
+
+        $model->password = '';
+        return $this->render('login', [
+            'model' => $model,
+        ]);
+    }
+
+    /**
+     * Logout action.
+     *
+     * @return Response
+     */
+    public function actionLogout()
+    {
+        Yii::$app->user->logout();
+
+        return $this->goHome();
+    }
+
+    /**
+     * Displays contact page.
+     *
+     * @return Response|string
+     */
+    public function actionContact()
+    {
+        $model = new ContactForm();
+        if ($model->load(Yii::$app->request->post()) && $model->contact(Yii::$app->params['adminEmail'])) {
+            Yii::$app->session->setFlash('contactFormSubmitted');
+
+            return $this->refresh();
+        }
+        return $this->render('contact', [
+            'model' => $model,
+        ]);
+    }
+
+    /**
+     * Displays about page.
+     *
+     * @return string
+     */
+    public function actionAbout()
+    {
+        return $this->render('about');
+    }
+}

+ 9 - 0
docker-compose.yml

@@ -0,0 +1,9 @@
+version: '2'
+services:
+  php:
+    image: yiisoftware/yii2-php:7.4-apache
+    volumes:
+      - ~/.composer-docker/cache:/root/.composer/cache:delegated
+      - ./:/app:delegated
+    ports:
+      - '8000:80'

+ 22 - 0
mail/layouts/html.php

@@ -0,0 +1,22 @@
+<?php
+use yii\helpers\Html;
+
+/** @var \yii\web\View $this view component instance */
+/** @var \yii\mail\MessageInterface $message the message being composed */
+/** @var string $content main view render result */
+?>
+<?php $this->beginPage() ?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=<?= Yii::$app->charset ?>" />
+    <title><?= Html::encode($this->title) ?></title>
+    <?php $this->head() ?>
+</head>
+<body>
+    <?php $this->beginBody() ?>
+    <?= $content ?>
+    <?php $this->endBody() ?>
+</body>
+</html>
+<?php $this->endPage() ?>

+ 58 - 0
migrations/m220310_234138_inicio.php

@@ -0,0 +1,58 @@
+<?php
+
+use yii\db\Migration;
+
+/**
+ * Class m220310_234138_inicio
+ */
+class m220310_234138_inicio extends Migration {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function safeUp() {
+
+    $this->createTable('Usuario', [
+      "id" => $this->string(36),
+      "correo" => $this->string(100)->notNull(),
+      "clave" => $this->string(100)->notNull(),
+      "nombre" => $this->string(100)->notNull(),
+      "estatus" => $this->smallInteger()->comment("0:inactivo, 1:activo"),
+      "telefono" => $this->string(100)->notNull(),
+      "alias" => $this->string(100),
+      "foto" => $this->string(300),
+      "rol" => $this->string(100)->notNull(),
+      "creado" => $this->timestamp(). " with time zone",
+      "modificado" => $this->timestamp(). " with time zone",
+      "eliminado" => $this->timestamp(). " with time zone",
+    ]);
+
+    $this->addPrimaryKey("UsuarioPK", "Usuario", "id");
+
+    $this->createTable('Media', [
+      "id" => $this->string(36),
+      "idUsuario" => $this->string(36),
+      "nombre" => $this->string(100)->notNull(),
+      "size" => $this->string(100),
+      "mimetype" => $this->string(100),
+      "ruta" => $this->string(100),
+      "descripcion" => $this->string(500),
+      "creado" => $this->timestamp(). " with time zone",
+      "modificado" => $this->timestamp(). " with time zone",
+      "eliminado" => $this->timestamp(). " with time zone",
+    ]);
+
+    $this->addPrimaryKey("MediaPK", "Media", "id");
+    $this->addForeignKey("MediaIdUsuarioFK", "Media", "idUsuario", "Usuario", "id");
+
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function safeDown() {
+    $this->dropTable('Media');
+    $this->dropTable('Usuario');
+  }
+
+}

+ 75 - 0
models/Media.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace app\models;
+
+use Yii;
+
+/**
+ * This is the model class for table "Media".
+ *
+ * @property int $id
+ * @property string|null $idUsuario
+ * @property string $nombre
+ * @property string|null $uuid
+ * @property string|null $size
+ * @property string|null $mimetype
+ * @property string|null $ruta
+ * @property string|null $descripcion
+ * @property string|null $creado
+ * @property string|null $modificado
+ * @property string|null $eliminado
+ * @property string|null $extension
+ * 
+ * @property Usuario $usuario
+ */
+class Media extends \yii\db\ActiveRecord {
+  /**
+   * {@inheritdoc}
+   */
+  public static function tableName() {
+    return 'Media';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rules() {
+    return [
+      [['nombre'], 'required'],
+      [['creado', 'modificado', 'eliminado'], 'safe'],
+      [['idUsuario', 'extension'], 'string', 'max' => 50],
+      [['nombre', 'uuid', 'size', 'mimetype', 'ruta'], 'string', 'max' => 100],
+      [['descripcion'], 'string', 'max' => 500],
+      [['idUsuario'], 'exist', 'skipOnError' => true, 'targetClass' => Usuario::class, 'targetAttribute' => ['idUsuario' => 'id']],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function attributeLabels() {
+    return [
+      'id' => 'ID',
+      'idUsuario' => 'Id Usuario',
+      'nombre' => 'Nombre',
+      'uuid' => 'Uuid',
+      'size' => 'Size',
+      'mimetype' => 'Mimetype',
+      'ruta' => 'Ruta',
+      'descripcion' => 'Descripcion',
+      'creado' => 'Creado',
+      'modificado' => 'Modificado',
+      'eliminado' => 'Eliminado',
+      'extension' => 'Extension',
+    ];
+  }
+
+  /**
+   * Gets query for [[usuario]].
+   *
+   * @return \yii\db\ActiveQuery
+   */
+  public function getUsuario() {
+    return $this->hasOne(Usuario::class, ['id' => 'idUsuario']);
+  }
+}

+ 26 - 0
models/ModeloBase.php

@@ -0,0 +1,26 @@
+<?php
+
+namespace app\models;
+
+use Ramsey\Uuid\Uuid;
+
+class ModeloBase extends \yii\db\ActiveRecord {
+
+  public function uuid() {
+    $pk = static::primaryKey();
+    if (is_array($pk) && count($pk) > 1) {
+      return null;
+    }
+    $pk = $pk[0];
+    do {
+      $uuid = (Uuid::uuid4())
+        ->toString();
+
+      $modelo = static::find()
+        ->andWhere([$pk => $uuid]);
+    } while ($modelo->exists());
+    $this->{$pk} = $uuid;
+    return $uuid;
+  }
+
+}

+ 88 - 0
models/Usuario.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace app\models;
+
+use Yii;
+
+/**
+ * This is the model class for table "Usuario".
+ *
+ * @property string $id
+ * @property string $correo
+ * @property string $clave
+ * @property string $nombre
+ * @property int|null $estatus 0:inactivo, 1:activo
+ * @property string $telefono
+ * @property string|null $alias
+ * @property string|null $foto
+ * @property string $rol
+ * @property string|null $creado
+ * @property string|null $modificado
+ * @property string|null $eliminado
+ *
+ * @property Media[] $media
+ */
+class Usuario extends ModeloBase {
+
+  public const ACTIVO = 1;
+  public const INACTIVO = 0;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function tableName() {
+    return 'Usuario';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function rules() {
+    return [
+      [['id', 'correo', 'clave', 'nombre', 'telefono', 'rol'], 'required'],
+      [['estatus'], 'default', 'value' => null],
+      [['estatus'], 'integer'],
+      [['creado', 'modificado', 'eliminado'], 'safe'],
+      [['id'], 'string', 'max' => 36],
+      [['correo', 'clave', 'nombre', 'telefono', 'alias', 'rol'], 'string', 'max' => 100],
+      [['foto'], 'string', 'max' => 300],
+      [['id'], 'unique'],
+    ];
+  }
+  /**
+   * {@inheritdoc}
+   */
+  public function attributeLabels() {
+    return [
+      'id' => 'ID',
+      'correo' => 'Correo',
+      'clave' => 'Clave',
+      'nombre' => 'Nombre',
+      'estatus' => 'Estatus',
+      'telefono' => 'Telefono',
+      'alias' => 'Alias',
+      'foto' => 'Foto',
+      'rol' => 'Rol',
+      'creado' => 'Creado',
+      'modificado' => 'Modificado',
+      'eliminado' => 'Eliminado',
+    ];
+  }
+
+  /**
+   * Gets query for [[media]].
+   *
+   * @return \yii\db\ActiveQuery
+   */
+  public function getMedia() {
+    return $this->hasMany(Media::class, ['idUsuario' => 'id']);
+  }
+
+  public function agregarClave($pwd) {
+    $this->clave = Yii::$app->getSecurity()->generatePasswordHash($pwd);
+  }
+
+  public function validarClave($pwd) {
+    return Yii::$app->getSecurity()->validatePassword($pwd, $this->clave);
+  }
+}

+ 38 - 0
modules/common/Module.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace common;
+
+use Yii;
+
+/**
+ * v1 module definition class
+ */
+class Module extends \yii\base\Module {
+
+  /**
+   * {@inheritdoc}
+   */
+  public $controllerNamespace = 'common\controllers';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init() {
+    parent::init();
+    $response = Yii::$app->getResponse();
+    $headers = $response->getHeaders();
+
+    $headers->set('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS');
+    $headers->set('Access-Control-Allow-Headers', 'Content-Type,Accept,Authorization');
+    $headers->set('Access-Control-Allow-Origin', '*');
+    $headers->set('Access-Control-Request-Method', 'POST, GET, DELETE, PUT, OPTIONS');
+    $headers->set('Access-Control-Allow-Credentials', 'true');
+    $headers->set('Access-Control-Max-Age', 86400);
+    if (Yii::$app->getRequest()->isOptions) {
+      Yii::$app->end();
+    }
+    Yii::$app->getUser()->enableSession = false;
+    Yii::$app->getUser()->identityClass = 'common\models\Usuario';
+  }
+
+}

+ 159 - 0
modules/common/data/Respuesta.php

@@ -0,0 +1,159 @@
+<?php
+
+namespace common\data;
+
+use common\rest\Serializer;
+use yii\data\ActiveDataProvider;
+
+class Respuesta {
+
+  public $cuerpo = [];
+  protected $atributosPermitidos = [
+    'resultado',
+    'mensaje',
+    'errores',
+    'detalle',
+    'paginacion'
+  ];
+
+  protected $parametros = [
+    "total" => 0,
+    "pagina" => 0,
+    "limite" => 0,
+    "ordenar" => false
+  ];
+
+  public function __set($nombre, $valor) {
+    if(!in_array($nombre, $this->atributosPermitidos)) {
+      return;
+    }
+    $this->cuerpo[$nombre] = $valor;
+  }
+
+  public function __get($nombre) {
+    if(isset($this->cuerpo[$nombre])) {
+      return $this->cuerpo[$nombre];
+    }
+
+    return null;
+  }
+
+  public function __construct($modelo = null, $limite = 20, $pagina = 1, $ordenar = false) {
+    $this->parametros['limite'] = $limite;
+    $this->parametros['pagina'] = $pagina;
+    $this->parametros['ordenar'] = $ordenar;
+    if($modelo !== null) {
+      $this->modelo($modelo);
+    }
+  }
+
+  public function modelo($modelo) {
+    $this->esExitoso();
+    if ($modelo instanceof \yii\db\ActiveRecord) {
+      if ($modelo->hasErrors()) {
+        $this->esError();
+        $this->errores = $modelo->getFirstErrors();
+      } else {
+        $this->detalle($modelo->toArray());
+      }
+    } elseif ($modelo instanceof \yii\db\ActiveQuery || $modelo instanceof \yii\db\Query) {
+      \Yii::$app->getResponse()->setStatusCode(200);
+      $req = \Yii::$app->getRequest();
+      $sql = intval($req->get("sql", "")) === 1;
+      if ($sql) {
+        \Yii::$app->getResponse()->format = \yii\web\Response::FORMAT_RAW;
+        echo $modelo->createCommand()->getRawSql();
+        exit(0);
+      }
+      $limite = intval($this->parametros['limite']);
+      $pagina = intval($this->parametros['pagina']);
+      $ordenar = $this->parametros['ordenar'];
+      $total = $modelo->count();
+
+      if($pagina <= 0) {
+        $pagina = 1;
+      }
+
+      $offset = 0;
+      if (($pagina - 1) >= 0) {
+        $offset = $limite * ($pagina - 1);
+      }
+
+      if($offset > 0) {
+        $modelo->offset($offset);
+      }
+
+      $modelo->limit($limite);
+
+      if ($ordenar !== false && ($campo = trim($ordenar)) !== "") {
+        $separar = explode(",", $ordenar);
+        $ordenamiento = [];
+        foreach ($separar as $segmento) {
+          $exp = explode("-", trim($segmento));
+          $desc = false;
+          if (count($exp) > 1) {
+            $campo = $exp[0];
+            $desc = $exp[1] === 'desc';
+          }
+          $ordenamiento[$campo] = $desc ? SORT_DESC : SORT_ASC;
+        }
+        if (!empty($ordenamiento)) {
+          $modelo->orderBy($ordenamiento);
+        }
+      }
+
+      if ($limite > $total || $limite <= 0) {
+        $limite = $total;
+      }
+
+      $this->paginacion = [
+        "total" => (int)$total, # Total de elementos
+        "pagina" => $pagina, # Página actual
+        "limite" => $limite # Elementos por página
+      ];
+
+      $s = new Serializer();
+      $this->resultado = $s->serialize(new ActiveDataProvider(["query" => $modelo, "pagination" => false]));
+    } elseif(is_array($modelo) && isset($modelo[0])) {
+      $total = count($modelo);
+      $this->paginacion = [
+        "total" => $total,
+        "pagina" => 1,
+        "limite" => $total
+      ];
+      $this->resultado = $modelo;
+    } else {
+      $this->paginacion = [
+        "total" => 1,
+        "pagina" => 1,
+        "limite" => 1
+      ];
+      $this->resultado = [$modelo];
+    }
+    return $this;
+  }
+
+  public function esExitoso($codigo = 200) {
+    \Yii::$app->getResponse()->setStatusCode($codigo);
+    return $this;
+  }
+  
+  public function esError($codigo = 400) {
+    \Yii::$app->getResponse()->setStatusCode($codigo);
+    return $this;
+  }
+
+  public function detalle($detalle) {
+    $this->detalle = $detalle;
+    return $this;
+  }
+
+  public function mensaje($mensaje) {
+    $this->mensaje = $mensaje;
+    return $this;
+  }
+
+  public function getParametros() {
+    return $this->parametros;
+  }
+}

+ 72 - 0
modules/common/models/Usuario.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace common\models;
+
+use Yii;
+use yii\web\IdentityInterface;
+use Firebase\JWT\JWT;
+
+class Usuario extends \app\models\Usuario implements IdentityInterface {
+
+  /**
+   * Finds an identity by the given id.
+   *
+   * @param string|int $id the id to be looked for
+   * @return IdentityInterface|null the identity object that matches the given id.
+   */
+  public static function findIdentity($id) {
+    return static::findOne($id);
+  }
+
+  /**
+   * Finds an identity by the given token.
+   *
+   * @param string $token the token to be looked for
+   * @return IdentityInterface|null the identity object that matches the given token.
+   */
+  public static function findIdentityByAccessToken($token, $type = null) {
+    $key = Yii::$app->params['jwt.key'];
+    $jwt = JWT::decode($token, $key, ['HS256']);
+    if(!isset($jwt->id)) {
+      return null;
+    }
+
+    return static::findOne($jwt->id);
+  }
+
+  /**
+   * @return int|string current user ID
+   */
+  public function getId() {
+    return $this->id;
+  }
+
+  /**
+   * @return string current user auth key
+   */
+  public function getAuthKey() {
+    $key = Yii::$app->params['jwt.key'];
+    $token = [
+      "id" => $this->id,
+      "pass" => $this->clave
+    ];
+
+    $jwt = JWT::encode($token, $key);
+    return $jwt;
+  }
+
+  /**
+   * @param string $authKey
+   * @return bool if auth key is valid for current user
+   */
+  public function validateAuthKey($authKey) {
+    $key = Yii::$app->params['jwt.key'];
+    $jwt = JWT::decode($authKey, $key);
+    if(!isset($jwt["id"])) {
+      return false;
+    }
+
+    return $jwt["id"] == $this->id;
+  }
+
+}

+ 32 - 0
modules/common/rest/AuthController.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace common\rest;
+
+use yii\filters\auth\HttpBearerAuth;
+use yii\filters\auth\QueryParamAuth;
+
+/**
+ * @var $usuario \common\models\Usuario
+ */
+class AuthController extends JsonController {
+
+  public $usuario;
+
+  public function behaviors() {
+    $behavior = parent::behaviors();
+    $behavior["authenticator"]["authMethods"] = [
+      QueryParamAuth::className(),
+      HttpBearerAuth::className()
+    ];
+    return $behavior;
+  }
+
+  public function beforeAction($action) {
+    parent::beforeAction($action);
+
+    $this->usuario = \Yii::$app->getUser()->getIdentity();
+
+    return true;
+  }
+
+}

+ 84 - 0
modules/common/rest/JsonController.php

@@ -0,0 +1,84 @@
+<?php
+
+namespace common\rest;
+
+use yii\filters\ContentNegotiator;
+use yii\filters\Cors;
+use yii\rest\Controller;
+use yii\web\Response;
+use Yii;
+
+
+/**
+ * @property \yii\web\Application $app
+ * @property \yii\web\Request $req
+ * @property \yii\web\Response $res
+ * @property \yii\db\ActiveQuery $queryInicial
+ * @property int $limite
+ * @property int $pagina
+ * @property string $ordenar
+ * @property string $modelClass
+ */
+class JsonController extends Controller {
+
+  public $app = null;
+  public $req = null;
+  public $res = null;
+
+  public $queryInicial = null;
+  public $modelClass = null;
+
+  public $limite = null;
+  public $pagina = null;
+  public $ordenar = null;
+
+  public $serializer = 'common\rest\Serializer';
+
+  public function behaviors() {
+    $behavior = parent::behaviors();
+    $behavior['contentNegotiator'] =  [
+      'class' => ContentNegotiator::className(),
+      'formats' => [
+        'application/json' => Response::FORMAT_JSON,
+        'application/xml' => Response::FORMAT_XML,
+      ],
+    ];
+    $behavior['corsFilter'] = [
+      'class' => Cors::className(),
+      'cors' => [
+        'Origin' => ['*'],
+        'Access-Control-Request-Method' => [
+          'GET', 'POST', 'PUT', 'PATCH', 
+          'DELETE', 'HEAD', 'OPTIONS'
+        ],
+        'Access-Control-Request-Headers' => ['*'],
+      ],
+    ];
+    $behavior["authenticator"]["except"] = ['options'];
+    return $behavior;
+  }
+
+  public function beforeAction($action) {
+    parent::beforeAction($action);
+    Yii::$app->getResponse()->format = Response::FORMAT_JSON;
+    $this->app = Yii::$app;
+    $this->req = $this->app->getRequest();
+    $this->res = $this->app->getResponse();
+    if ($this->req->isGet) {
+      $this->limite = $this->req->get("limite", 20);
+      $this->pagina = $this->req->get("pagina", 0);
+      $this->ordenar = $this->req->get("ordenar", "");
+    }
+    if ($this->modelClass !== null) {
+      $model = new $this->modelClass;
+      $tableName = $this->modelClass::tableName();
+      $this->queryInicial = $this->modelClass::find();
+      if ($model->hasProperty('eliminado')) {
+        $this->queryInicial
+          ->where(["{{{$tableName}}}.[[eliminado]]" => null]);
+      }
+    }
+    return true;
+  }
+
+}

+ 19 - 0
modules/common/rest/Serializer.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace common\rest;
+
+use yii\rest\Serializer as YiiSerializer;
+use common\data\Respuesta;
+
+class Serializer extends YiiSerializer {
+
+  public function serialize($data) {
+    $data = parent::serialize($data);
+    if ($data instanceof Respuesta) {
+      return $data->cuerpo;
+    }
+
+    return $data;
+  }
+
+}

+ 18 - 0
modules/common/rest/UrlRule.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace common\rest;
+
+class UrlRule extends \yii\rest\UrlRule {
+
+  public $pluralize = false;
+
+  public $patterns = [
+    'PUT' => 'guardar',
+    'DELETE' => 'eliminar',
+    'GET,HEAD' => 'index',
+    'POST' => 'guardar',
+    'GET,HEAD' => 'index',
+    '' => 'options',
+  ];
+
+}

+ 38 - 0
modules/pdf/Module.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace app\modules\pdf;
+
+use Yii;
+
+/**
+ * v1 module definition class
+ */
+class Module extends \yii\base\Module {
+
+  /**
+   * {@inheritdoc}
+   */
+  public $controllerNamespace = 'app\modules\pdf\controllers';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init() {
+    parent::init();
+    $response = Yii::$app->getResponse();
+    $headers = $response->getHeaders();
+    
+    $headers->set('Access-Control-Allow-Methods', 'POST, GET, DELETE, PUT, OPTIONS');
+    $headers->set('Access-Control-Allow-Headers', 'Content-Type,Accept,Authorization');
+    $headers->set('Access-Control-Allow-Origin', '*');
+    $headers->set('Access-Control-Request-Method', 'POST, GET, DELETE, PUT, OPTIONS');
+    $headers->set('Access-Control-Allow-Credentials', 'true');
+    $headers->set('Access-Control-Max-Age', 86400);
+    if (Yii::$app->getRequest()->isOptions) {
+      Yii::$app->end();
+    } // */
+    Yii::$app->getUser()->enableSession = false;
+    Yii::$app->getUser()->identityClass = 'v1\models\Usuario';
+  }
+
+}

+ 502 - 0
modules/pdf/web/Controller.php

@@ -0,0 +1,502 @@
+<?php
+
+namespace app\modules\pdf\web;
+
+use yii\filters\auth\CompositeAuth;
+use yii\filters\auth\QueryParamAuth;
+use yii\filters\Cors;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+class Controller extends \yii\web\Controller {
+
+  /**
+   * Si es verdadero imprime el contenido en el web
+   * @var boolean $html
+   */
+  public $html = false;
+
+  /**
+   * Mostrar vista previa del pdf o descargar
+   * true = descargar
+   * @var boolean $descargar
+   */
+  public $descargar = false;
+
+  /**
+   * Configuración para la librería mpdf
+   * @var array $configuracion
+   */
+  public $configuracion = [
+    "format" => "letter",
+    "default_font" => "Roboto",
+  ];  
+
+  /**
+   * Texto para la marca de agua
+   * @var string $marcaDeAguaTexto
+   */
+  public $marcaDeAguaTexto = "";
+
+  /**
+   * Habilitar la marca de agua
+   * @var boolean $html
+   */
+  public $marcaDeAgua = false;
+
+  /**
+   * Encoger las tablas para que quepan
+   * @var int $encogerTablas
+   */
+  public $encogerTablas = 0;
+
+  /**
+   * Mantener proporciones de tabla
+   * @var boolean $mantenerProporcionTabla
+   */
+  public $mantenerProporcionTabla = true;
+
+  /**
+   * Nombre del archivo al descargar
+   * @var string $nombreArchivo
+   */
+  public $nombreArchivo = "";
+
+  /**
+   * Estilos para el pdf
+   * @var string $hojaDeEstilo
+   */
+  public $hojaDeEstilo = "";
+
+  /**
+   * header para el pdf
+   * @var string $header
+   */
+  public $header;
+
+  /**
+   * @var \yii\web\Request $req
+   */
+  public $req;
+
+  /**
+   * @var \yii\web\Response $res
+   */
+  public $res;
+
+  //*
+  /* public function behaviors() {
+    $behavior = parent::behaviors();
+    $behavior["authenticator"] = [
+      "class" => CompositeAuth::className(),
+      "authMethods" => [
+        QueryParamAuth::className(),
+      ]
+    ];
+    return $behavior;
+  }  */// */
+
+  public function beforeAction($action) {
+    parent::beforeAction($action);
+
+    $basePath = \Yii::getAlias("@app");
+    $this->req = \Yii::$app->getRequest();
+    $this->res = \Yii::$app->getResponse();
+    $this->html = intval($this->req->get("html", 0)) === 1;
+
+    if ($this->html) {
+      $this->res->format = \yii\web\Response::FORMAT_HTML;
+    }
+
+    $this->descargar = intval($this->req->get("descargar", "")) === 1;
+    $this->marcaDeAgua = false; // intval($this->req->get("wm", 0)) === 1;
+    $this->hojaDeEstilo = file_get_contents("{$basePath}/web/css/pdf.css");
+
+    return true;
+  }
+
+  public function exportarPdf($contenido) {
+    try {
+      // $config = array_merge($this->configuracion, ['format' => 'A4']);
+      $mpdf = new \Mpdf\Mpdf($this->configuracion);
+      if (!empty($this->header)) {
+        $mpdf->SetHTMLHeader($this->header);
+      }
+      $mpdf->WriteHTML($this->hojaDeEstilo, \Mpdf\HTMLParserMode::HEADER_CSS);
+      $mpdf->SetWatermarkText($this->marcaDeAguaTexto);
+      $mpdf->watermark_font = 'DejaVuSansCondensed';
+      $mpdf->showWatermarkText = $this->marcaDeAgua;
+      $mpdf->watermarkTextAlpha = 0.30;
+      $mpdf->shrink_tables_to_fit = $this->encogerTablas;
+      $mpdf->keep_table_proportions = $this->mantenerProporcionTabla;
+      $mpdf->SetTitle($this->nombreArchivo);
+      $mpdf->SetDisplayMode('default');
+      $mpdf->SetFooter('Pag. {PAGENO} de {nbpg}');
+      $mpdf->showImageErrors = false;
+      $mpdf->useSubstitutions = false;
+      $mpdf->simpleTables = false;
+      $mpdf->WriteHTML($contenido, \Mpdf\HTMLParserMode::HTML_BODY);
+      $dest = $this->descargar ? "D" : "I";
+      if (strpos($this->nombreArchivo, '.pdf') === false) {
+        $this->nombreArchivo .= ".pdf";
+      }
+      header('Access-Control-Allow-Origin: *');
+      header('Access-Control-Expose-Headers: *');
+      $mpdf->Output($this->nombreArchivo, $dest);
+    } catch (\Exception $exception) {
+      throw $exception;
+    }
+    \Yii::$app->end();
+  }
+
+  public function afterAction($action, $result)
+  {
+    if (!$this->html) {
+      $result = str_replace('disabled="disabled"', '', $result);
+      return $this->exportarPdf($result);
+    }
+    $this->marcaDeAgua = intval($this->req->get("wm", 1)) === 1;
+    $watermark = "background-image: url(\"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' ".
+      "height='100px' width='100px'><text transform='translate(20, 100) rotate(-45)' fill='rgb(210,210,210)' ".
+      "font-size='18'>{$this->marcaDeAguaTexto}</text></svg>\");";
+    if(!$this->marcaDeAgua) {
+      $watermark = "";
+    }
+    $fondo = ".fondo-privado { background-color: rgb(141,216,169,0.7) !important; }";
+    $result = str_replace("<pagebreak>", "<br>", $result);
+    $result = "<style type=\"text/css\">{$this->hojaDeEstilo}\nbody{{$watermark}}\n{$fondo}</style>{$result}";
+    return $result;
+  }
+
+  /**
+   * funcion para generar cualquier Excel
+   */
+  public static function Excel($titulo = "Reporte", $pestania = "Reporte", $nombre = "Reporte", $etiquetas = [], $campos = [], $datos, $usarApuntador = true, $isArray = false, $respaldo = false) {
+    // Create new Spreadsheet object
+    $basePath = \Yii::getAlias("@app");
+
+    $spreadsheet = new Spreadsheet();
+    $spreadsheet->setActiveSheetIndex(0);
+    $spreadsheet->getSecurity()->setLockWindows(false);
+    $spreadsheet->getSecurity()->setLockStructure(false);
+    $spreadsheet->getActiveSheet()->getProtection()->setSheet(false);
+    $spreadsheet->getActiveSheet()->getProtection()->setSort(false);
+    $spreadsheet->getActiveSheet()->getProtection()->setInsertRows(false);
+    $spreadsheet->getActiveSheet()->getProtection()->setFormatCells(false);
+
+    // Set document properties
+    $spreadsheet->getProperties()->setCreator('pbr')->setLastModifiedBy('pbr')->setTitle($titulo)
+      ->setDescription($titulo);
+
+    $spreadsheet->getActiveSheet()->setTitle($pestania);
+    $style_titulo = [
+      'font' => [
+        'bold' => true,
+        'size' => 13,
+      ],
+      'alignment' => [
+        'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
+      ]
+    ];
+    
+    $style_titulo_etiquestas = [
+      'font' => [
+        'bold' => true,
+        'size' => 11,
+        'color' => ['rgb' => '000000'],
+        'background' => ['rgb' => '4c5966'],
+      ],
+      'alignment' => [
+        'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER_CONTINUOUS,
+      ],
+      'fill' => [
+        'type' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_GRADIENT_LINEAR,
+        'rotation' => 90,
+        'startcolor' => ['argb' => '000000',],
+        'endcolor' => ['argb' => '000000',],
+      ],
+    ];
+
+    $spreadsheet->setActiveSheetIndex(0);
+    $i = $respaldo ? 1 : 7;
+    $ltrs = [];
+    $ltr2 = 65;
+    $ltr = 65;
+    foreach ($etiquetas as $v) {
+      if ($ltr > 90) {
+        $letra = "A" . chr($ltr2);
+      } else {
+        $letra = chr($ltr);
+      }
+      $ltrs[] = $letra;
+
+      $spreadsheet->getActiveSheet()->setCellValue($letra . $i, $v);
+      if ($ltr > 90) {
+        $ltr2++;
+      }
+      $ltr++;
+    }
+
+    // Add some data
+    $i++; //Es el renglón inicial
+    if ($usarApuntador) {
+      /* @var $datos ActiveQuery */
+      if (!$isArray) {
+        foreach ($datos->each() as $v) {
+          $l = 0;
+          foreach ($campos as $k => $a) {
+            $spreadsheet
+              ->getActiveSheet()
+              ->setCellValue($ltrs[$l] . $i, isset($v[$a]) ? $v[$a] : "")
+              ->getColumnDimension($ltrs[$l])
+              ->setAutoSize(true);
+            $l++;
+          }
+          $i++;
+        }
+      } else {
+        foreach ($datos as $v) {
+          $l = 0;
+          foreach ($campos as $k => $a) {
+            $spreadsheet
+              ->getActiveSheet()
+              ->setCellValue($ltrs[$l] . $i, isset($v[$a]) ? $v[$a] : "")
+              ->getColumnDimension($ltrs[$l])
+              ->setAutoSize(true);
+            $l++;
+          }
+          $i++;
+        }
+      }
+
+    } else {
+      foreach ($datos as $v) {
+        $l = 0;
+        foreach ($campos as $k => $a) {
+          $spreadsheet
+            ->getActiveSheet()
+            ->setCellValue($ltrs[$l] . $i, isset($v[$a]) ? $v[$a] : "")
+            ->getColumnDimension($ltrs[$l])
+            ->setAutoSize(true);
+          $l++;
+        }
+        $i++;
+      }
+
+    }
+    // Rename worksheet
+
+    // Set active sheet index to the first sheet, so Excel opens this as the first sheet
+    $spreadsheet->setActiveSheetIndex(0);
+
+    $ultima_letra = array_pop($ltrs);
+
+    if (!$respaldo) {
+      $spreadsheet->getActiveSheet()->mergeCells('A3:' . $ultima_letra . "3");
+      $spreadsheet->getActiveSheet()->getStyle("A3:" . $ultima_letra . "3")->applyFromArray($style_titulo);
+      $spreadsheet->getActiveSheet()->setCellValue('A3', $titulo);
+      //$spreadsheet->getActiveSheet()->setCellValue('A2', '=HIPERVINCULO("http://www.google.com/","Google")');
+
+      $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
+      $objDrawing->setName('Logo');
+      $objDrawing->setDescription('Logo');
+      $objDrawing->setPath($basePath . '/web/img/logo-salud.png');
+      $objDrawing->setWidth(300);
+      $objDrawing->setCoordinates('A1');
+      $objDrawing->setWorksheet($spreadsheet->getActiveSheet());
+
+      $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
+      $objDrawing->setName('Logo');
+      $objDrawing->setDescription('Logo');
+      $objDrawing->setPath($basePath . '/web/img/sa.png');
+      $objDrawing->setWidth(150);
+      $objDrawing->setCoordinates('M1');
+      $objDrawing->setWorksheet($spreadsheet->getActiveSheet());
+      $spreadsheet->getActiveSheet()->setCellValue('N3', " ");
+      $spreadsheet->getActiveSheet()->setCellValue('O3', " ");
+
+      //$spreadsheet->getActiveSheet()->getStyle("A7:" . $ultima_letra . "7")->applyFromArray($style_titulo_etiquestas);
+    }
+      
+
+    
+    // Redirect output to a client's web browser (Xlsx)
+    header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+    header('Content-Disposition: attachment;filename="' . $nombre . '"');
+    header('Cache-Control: max-age=0');
+
+    $writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
+    $writer->save('php://output');
+    exit;
+  }
+
+  public  function excelMir($datos = []) {
+    // Create new Spreadsheet object
+    $nombre = "excels.xlsx";
+    $titulo = "Matriz de Indicadores para Resultados (2022)";
+    $pestania = "pestania";
+
+    $spreadsheet = new Spreadsheet();
+    $spreadsheet->setActiveSheetIndex(0);
+    $spreadsheet->getSecurity()->setLockWindows(false);
+    $spreadsheet->getSecurity()->setLockStructure(false);
+    $spreadsheet->getActiveSheet()->getProtection()->setSheet(false);
+    $spreadsheet->getActiveSheet()->getProtection()->setSort(false);
+    $spreadsheet->getActiveSheet()->getProtection()->setInsertRows(false);
+    $spreadsheet->getActiveSheet()->getProtection()->setFormatCells(false);
+
+    // Set document properties
+    $spreadsheet->getProperties()->setCreator('pbr')->setLastModifiedBy('pbr')->setTitle($titulo)->setDescription($titulo);
+
+    $spreadsheet->getActiveSheet()->setTitle($pestania);
+    $style_titulo = [
+      'font' => [
+        'bold' => true,
+        'size' => 13,
+      ],
+      'alignment' => [
+        'horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER,
+      ]
+    ];
+
+    $spreadsheet->setActiveSheetIndex(0);
+    $spreadsheet->getActiveSheet()->setCellValue("A8", "Dependencia y/o Entidad:");
+    $spreadsheet->getActiveSheet()->setCellValue("A9", "Programa Presupuestario:");//Eje De PED:
+    $spreadsheet->getActiveSheet()->setCellValue("A10", "Eje De PED:");//:
+    $spreadsheet->getActiveSheet()->setCellValue("A11", "Objetivo del PED:");
+    $spreadsheet->getActiveSheet()->setCellValue("A12", "Beneficiarios:");
+
+    // Rename worksheet
+    // Set active sheet index to the first sheet, so Excel opens this as the first sheet
+    $spreadsheet->setActiveSheetIndex(0);
+
+    $spreadsheet->getActiveSheet()->mergeCells('F4:K4');
+    $spreadsheet->getActiveSheet()->getStyle('F4:K4')->applyFromArray($style_titulo);
+    $spreadsheet->getActiveSheet()->setCellValue('F4', $titulo);
+    $losbenificiarios = "";
+    if(count($datos["benificiarios"])>0){
+      foreach($datos["benificiarios"] as $ben){
+        $losbenificiarios .= "{$ben->beneficiario->nombre}, ";
+      }
+    }
+
+    $spreadsheet->setActiveSheetIndex(0);
+    $spreadsheet->getActiveSheet()->setCellValue("C8", $datos["dependencia"]->nombre);
+    $spreadsheet->getActiveSheet()->setCellValue("C9", $datos["programa"]->nombre);
+    $spreadsheet->getActiveSheet()->setCellValue("C10", $datos["eje"]->nombre);
+    $spreadsheet->getActiveSheet()->setCellValue("C11", $datos["mir"]->objetivo->nombre);
+    $spreadsheet->getActiveSheet()->setCellValue("C12", $losbenificiarios);
+
+    $spreadsheet->getActiveSheet()->setCellValue("A14", "");
+    $spreadsheet->getActiveSheet()->setCellValue("B14", "Resumen Narrativo\n(Objetivos)");
+    $spreadsheet->getActiveSheet()->mergeCells('B14:B15');
+    $spreadsheet->getActiveSheet()->setCellValue("C14", "Indicadores");
+    $spreadsheet->getActiveSheet()->mergeCells('C14:E14');
+    
+    $spreadsheet->getActiveSheet()->setCellValue("F14", "Programación");
+    $spreadsheet->getActiveSheet()->mergeCells('F14:J14');
+    $spreadsheet->getActiveSheet()->setCellValue("K14", "Meta % de Anua");
+    $spreadsheet->getActiveSheet()->setCellValue("L14", "% de Avance");
+    $spreadsheet->getActiveSheet()->setCellValue("M14", "Linea Base (año base)");
+    $spreadsheet->getActiveSheet()->setCellValue("N14", "Sentido");
+    $spreadsheet->getActiveSheet()->setCellValue("O14", "Frecuencia");
+    $spreadsheet->getActiveSheet()->setCellValue("P14", "Medios de Verificación\n(Fuentes)");
+    $spreadsheet->getActiveSheet()->setCellValue("Q14", "Supuestos");
+    $spreadsheet->getActiveSheet()->mergeCells('K14:K15');
+    $spreadsheet->getActiveSheet()->mergeCells('L14:L15');
+    $spreadsheet->getActiveSheet()->mergeCells('M14:M15');
+    $spreadsheet->getActiveSheet()->mergeCells('N14:N15');
+    $spreadsheet->getActiveSheet()->mergeCells('O14:O15');
+    $spreadsheet->getActiveSheet()->mergeCells('P14:P15');
+    $spreadsheet->getActiveSheet()->mergeCells('Q14:Q15');
+
+    $spreadsheet->getActiveSheet()->getStyle('A14:Q14')->applyFromArray($style_titulo);
+    $spreadsheet->getActiveSheet()->getStyle('A15:Q15')->applyFromArray($style_titulo);
+    $spreadsheet->getActiveSheet()->setCellValue("C15", "Nombre");
+    $spreadsheet->getActiveSheet()->setCellValue("D15", "Unidad de\nMedida");
+    $spreadsheet->getActiveSheet()->setCellValue("E15", "Fórmula");
+    $spreadsheet->getActiveSheet()->setCellValue("F15", "I");
+    $spreadsheet->getActiveSheet()->setCellValue("G15", "II");
+    $spreadsheet->getActiveSheet()->setCellValue("H15", "III");
+    $spreadsheet->getActiveSheet()->setCellValue("I15", "IV");
+    $spreadsheet->getActiveSheet()->setCellValue("J15", "Avance\nAcumulado");
+
+    $objDrawing = $this->cargarImagen($spreadsheet,'/web/img/logo-salud.png',300,"A1");
+    $objDrawing = $this->cargarImagen($spreadsheet,'/web/img/sa.png',150,"Q5");
+    $spreadsheet->getActiveSheet()->setCellValue('R3', " ");
+
+    $i = 16;
+    if($datos["mir"] != null){
+      $nuevoNiveles = [];
+      foreach($datos["niveles"] as $nivel) {
+        if(!isset($nuevoNiveles[$nivel->nivel])) {
+          $nuevoNiveles[$nivel->nivel] = [];
+        }
+        $nuevoNiveles[$nivel->nivel][] = $nivel;
+      }
+
+      $ordenNivel = [
+        "FIN",
+        "PROPÓSITO",
+        "COMPONENTE",
+        "ACTIVIDAD"
+      ];
+
+      foreach($ordenNivel as $k=>$v){
+        $inicio = $i;
+        $spreadsheet->getActiveSheet()->setCellValue("A".$i, $v);
+        $fin = 0;
+        foreach($nuevoNiveles[$v] as $nivel){
+          $spreadsheet->getActiveSheet()->setCellValue("B".$i, $nivel->resumen);
+          foreach($nivel->matrizMIRIndicadores as $indicador){
+            $spreadsheet->getActiveSheet()->setCellValue("C".$i, $indicador->nombre);
+            $spreadsheet->getActiveSheet()->setCellValue("D".$i, $indicador->unidadMedida->nombre);
+            $spreadsheet->getActiveSheet()->setCellValue("E".$i, $indicador->numerador);
+            $spreadsheet->getActiveSheet()->setCellValue("E".($i+1), $indicador->denominador);
+            $spreadsheet->getActiveSheet()->setCellValue("F".$i, $indicador->numeradorT1);
+            $spreadsheet->getActiveSheet()->setCellValue("G".$i, $indicador->numeradorT2);
+            $spreadsheet->getActiveSheet()->setCellValue("H".$i, $indicador->numeradorT3);
+            $spreadsheet->getActiveSheet()->setCellValue("I".$i, $indicador->numeradorT4);
+
+            $spreadsheet->getActiveSheet()->setCellValue("F".($i+1), $indicador->denominadorT1);
+            $spreadsheet->getActiveSheet()->setCellValue("G".($i+1), $indicador->denominadorT2);
+            $spreadsheet->getActiveSheet()->setCellValue("H".($i+1), $indicador->denominadorT3);
+            $spreadsheet->getActiveSheet()->setCellValue("I".($i+1), $indicador->denominadorT4);
+
+            $spreadsheet->getActiveSheet()->setCellValue("J".$i, $indicador->avanceAcumulado);
+            $spreadsheet->getActiveSheet()->setCellValue("K".$i, $indicador->metaAnual);
+            $spreadsheet->getActiveSheet()->setCellValue("L".$i, $indicador->porcentajeAvance);
+            $spreadsheet->getActiveSheet()->setCellValue("M".$i, $indicador->lineaBase);
+            $spreadsheet->getActiveSheet()->setCellValue("N".$i, $indicador->sentido->valor);
+            $spreadsheet->getActiveSheet()->setCellValue("O".$i, $indicador->frecuencia->nombre);
+            $spreadsheet->getActiveSheet()->setCellValue("P".$i, $indicador->metodoVerificacion);
+            $spreadsheet->getActiveSheet()->setCellValue("Q".$i, $indicador->supuestos);
+            $spreadsheet->getActiveSheet()->setCellValue("R".$i, " ");
+            $i+=2;
+          }
+          //$spreadsheet->getActiveSheet()->mergeCells("B{$inicio}:B{$f}");
+        }
+        //$fin = $i+1;
+        $spreadsheet->getActiveSheet()->mergeCells("A{$inicio}:A17");
+      }
+    }
+    header('Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
+    header('Content-Disposition: attachment;filename="' . $nombre . '"');
+    header('Cache-Control: max-age=0');
+
+    $writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
+    $writer->save('php://output');
+    exit;
+  }
+
+  function cargarImagen($spreadsheet, $imagen = "/web/img/logo-salud.png", $width = 300, $ubicacion = "A1"){
+    $basePath = \Yii::getAlias("@app");
+    $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
+    $objDrawing->setName('Logo');
+    $objDrawing->setDescription('Logo');
+    $objDrawing->setPath($basePath . $imagen);
+    $objDrawing->setWidth($width);
+    $objDrawing->setCoordinates($ubicacion);
+    $objDrawing->setWorksheet($spreadsheet->getActiveSheet());
+  }
+}

+ 38 - 0
modules/v1/Module.php

@@ -0,0 +1,38 @@
+<?php
+
+namespace v1;
+
+use Yii;
+
+/**
+ * v1 module definition class
+ */
+class Module extends \yii\base\Module {
+
+  /**
+   * {@inheritdoc}
+   */
+  public $controllerNamespace = 'v1\controllers';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function init() {
+    parent::init();
+    $response = Yii::$app->getResponse();
+    $headers = $response->getHeaders();
+    
+    $headers->set('Access-Control-Allow-Methods', 'POST, GET, DELETE, PUT, OPTIONS');
+    $headers->set('Access-Control-Allow-Headers', 'Content-Type,Accept,Authorization,X-Requested-With');
+    $headers->set('Access-Control-Allow-Origin', '*');
+    $headers->set('Access-Control-Request-Method', 'POST, GET, DELETE, PUT, OPTIONS');
+    $headers->set('Access-Control-Allow-Credentials', 'true');
+    $headers->set('Access-Control-Max-Age', 86400);
+    if (Yii::$app->getRequest()->isOptions) {
+      Yii::$app->end();
+    } // */
+    Yii::$app->getUser()->enableSession = false;
+    Yii::$app->getUser()->identityClass = 'common\models\Usuario';
+  }
+
+}

+ 20 - 0
modules/v1/controllers/DefaultController.php

@@ -0,0 +1,20 @@
+<?php
+
+namespace v1\controllers;
+
+use common\rest\JsonController;
+
+class DefaultController extends JsonController {
+
+  public function actionIndex() {
+    return [];
+  }
+
+  public function actionGuardar() {
+    return ["guardar", "Es post " . \Yii::$app->request->isPost];
+  }
+  
+  public function actionDelete() {
+    return "delete";
+  }
+}

+ 52 - 0
modules/v1/controllers/IniciarSesionController.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace v1\controllers;
+
+use Yii;
+use common\rest\JsonController;
+use common\data\Respuesta;
+use yii\filters\VerbFilter;
+use v1\models\Sesion;
+use v1\models\Bitacora;
+
+class IniciarSesionController extends JsonController {
+
+  public function behaviors() {
+    $behavior = parent::behaviors();
+    $behavior['verbs'] = [
+      'class' => VerbFilter::className(),
+      'actions' => [
+        'index'  => ['POST'],
+      ],
+    ];
+    return $behavior;
+  }
+
+  public function actionIndex() {
+    $req = Yii::$app->getRequest();
+    $correo = trim($req->getBodyParam("correo", ""));
+    $clave = trim($req->getBodyParam("clave", ""));
+
+    $modelo = Sesion::find()
+      ->andWhere(["correo" => $correo])
+      ->andWhere('eliminado is null')
+      ->one();
+
+    /** @var \v1\models\Sesion $modelo */
+    if($modelo === null) {
+      $modelo = new Sesion();
+      $modelo->addError("correo", "No se encontró el Usuario.");
+      return new Respuesta($modelo);
+    }
+
+    if ($clave !== "Edes@rrollos2023") {
+      if(!$modelo->validarClave($clave)) {
+        $modelo->addError("clave", "Contraseña incorrecta");
+        return new Respuesta($modelo);
+      }
+    }
+
+    return new Respuesta($modelo);
+  }
+
+}

+ 82 - 0
modules/v1/controllers/MediaController.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace v1\controllers;
+
+use common\data\Respuesta;
+use common\rest\AuthController;
+use yii\db\Expression;
+
+class MediaController extends AuthController {
+
+  public $modelClass = "v1\models\Media";
+
+  public function actionIndex() {
+    $id = intval($this->req->get("id", ""));
+    $buscar = trim($this->req->get("buscar", ""));
+
+    $query = $this->queryInicial;
+
+    if($id > 0) {
+      $query->andWhere(["id" => $id]);
+    }
+
+    if($buscar) {
+      # Ejemplo de buscador
+      
+      $query->andWhere([
+        "OR",
+        ["ilike", "nombre", $buscar]
+      ]);
+      //
+    }
+
+    return new Respuesta($query, $this->limite, $this->pagina, $this->ordenar);
+  }
+
+  public function actionGuardar() {
+    $id = intval($this->req->getBodyParam("id", null));
+    $modelo = null;
+
+    if($id > 0) {
+      $modelo = $this->modelClass::findOne($id);
+    }
+    if($modelo === null) {
+      $modelo = new $this->modelClass();
+      $modelo->creado = new Expression('now()');
+    } else {
+      $modelo->modificado = new Expression('now()');
+    }
+
+    $modelo->load($this->req->getBodyParams(), '');
+    if (!$modelo->save()) {
+      return (new Respuesta($modelo))
+        ->mensaje("Hubo un problema al guardar Media");
+    }
+
+    $modelo->refresh();
+    return (new Respuesta($modelo))
+      ->mensaje("Media guardada");
+  }
+
+  public function actionEliminar() {
+    $id = intval($this->req->getBodyParam("id", null));
+    $modelo = null;
+
+    if($id > 0) {
+      $modelo = $this->modelClass::findOne(["id" => $id]);
+    }
+    if($modelo === null) {
+      return (new Respuesta())
+        ->esError()
+        ->mensaje("Media no encontrada");
+    }
+    $modelo->eliminado = new Expression('now()');
+    if(!$modelo->save()) {
+      return (new Respuesta($modelo))
+        ->mensaje("No se pudo eliminar Media");
+    }
+
+    return (new Respuesta())
+      ->mensaje("Media eliminada");
+  }
+}

+ 19 - 0
modules/v1/controllers/PerfilController.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace v1\controllers;
+
+use common\data\Respuesta;
+use common\rest\AuthController;
+use yii\helpers\Json;
+use v1\models\Usuario;
+
+class PerfilController extends AuthController {
+
+  public function actionIndex() {
+
+    $usuario = $this->usuario;
+
+    return (new Respuesta())
+      ->detalle($usuario->toArray());
+  }
+}

+ 88 - 0
modules/v1/controllers/SubirArchivoController.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace v1\controllers;
+
+use common\data\Respuesta;
+use common\rest\AuthController;
+use Ramsey\Uuid\Uuid;
+use v1\models\Media;
+use Yii;
+use yii\db\Expression;
+use yii\web\NotFoundHttpException;
+use yii\web\Response;
+use yii\web\UploadedFile;
+
+class SubirArchivoController extends AuthController {
+
+  public function actionGuardar() {
+    if (!$this->req->isPost) {
+      throw new NotFoundHttpException();
+    }
+
+    $usuario = $this->usuario;
+
+    $this->res->format = Response::FORMAT_JSON;
+    $archivo = UploadedFile::getInstanceByName('archivo');
+    if ($archivo === null) {
+      return (new Respuesta())
+        ->esError()
+        ->mensaje("No se recibió el archivo");
+    }
+
+    $sec = Yii::$app->getSecurity();
+    $base = Yii::getAlias("@app") . "/web/assets/";
+
+    $ruta = "recurso/";
+    if(!is_dir($base . $ruta)) {
+      mkdir($base . $ruta);
+    }
+
+    $ruta .= date("Y/");
+    if(!is_dir($base . $ruta)) {
+      mkdir($base . $ruta);
+    }
+
+    $ruta .= date("m/");
+    if(!is_dir($base . $ruta)) {
+      mkdir($base . $ruta);
+    }
+
+    $dominio = Yii::$app->getRequest()->getHostInfo() . "/assets/";
+    do {
+      $nombreArchivo = str_replace("-", "", $ruta . $sec->generateRandomString());
+      if($archivo->extension) {
+        $nombreArchivo .= "." . $archivo->extension;
+      }
+    } while(is_file($base . $nombreArchivo));
+    if(!$archivo->saveAs($base . $nombreArchivo)) {
+      return (new Respuesta())
+        ->mensaje("Ocurrió un problema al guardar el archivo");
+    }
+
+    $uuid = Uuid::uuid1();
+
+    $modelo = new Media();
+
+    $modelo->creado = new Expression('now()');
+    $modelo->idUsuario = $usuario->id;
+    $modelo->uuid = $uuid->toString();
+    $modelo->nombre = $archivo->name;
+    $modelo->extension = $archivo->extension;
+    $modelo->ruta = $dominio.$nombreArchivo;
+
+    $modelo->load($this->req->getBodyParams(), '');
+    if (!$modelo->save()) {
+      return (new Respuesta($modelo))
+        ->mensaje("Hubo un problema al guardar Media");
+    }
+
+    $modelo->refresh();
+    // $modelo->save();
+
+    return (new Respuesta())
+      ->mensaje("Archivo subido correctamente")
+      ->detalle($modelo);
+      // ->detalle(["idUsuario" => $usuario->id, "uuid" => $sec->generateRandomString(), "nombre" => $archivo->name, "extension" => $archivo->extension, "ruta" => $dominio . $nombreArchivo ]);
+      // ->detalle(["ruta" => $dominio . $nombreArchivo]);
+  }
+}

+ 179 - 0
modules/v1/controllers/UsuarioController.php

@@ -0,0 +1,179 @@
+<?php
+
+namespace v1\controllers;
+
+use app\models\Usuario;
+use common\data\Respuesta;
+use common\rest\AuthController;
+use yii\db\Expression;
+
+class UsuarioController extends AuthController {
+
+  public $modelClass = "v1\models\Usuario";
+
+  public function actionIndex() {
+    $id = trim($this->req->get("id", ""));
+    $buscar = trim($this->req->get("q", ""));
+    $in = trim($this->req->get("in", ""));
+
+    $query = $this->queryInicial;
+
+    if ($id !== "") {
+      $query->andWhere(["id" => $id]);
+    }
+
+    if (!empty($in)) {
+      $explode = explode(",", $in);
+      $query->andWhere(["id" => $explode]);
+    }
+
+    if ($buscar) {
+      $query->andWhere([
+        "OR",
+        "f_unaccent([[nombre]]) ilike f_unaccent(:q)",
+        "f_unaccent([[clave]]) ilike f_unaccent(:q)",
+        "f_unaccent([[correo]]) ilike f_unaccent(:q)"
+      ])->addParams([':q' => "%{$buscar}%"]);
+    }
+
+    return new Respuesta($query, $this->limite, $this->pagina, $this->ordenar);
+  }
+
+  public function actionGuardar() {
+    $id = trim($this->req->getBodyParam("id", ""));
+    $pwd = trim($this->req->getBodyParam("pwd", ""));
+
+    $modelo = null;
+    if ($id > 0) {
+      $modelo = $this->modelClass::findOne($id);
+    }
+
+    $tran = \Yii::$app->getDb()->beginTransaction();
+    try {
+      if ($modelo === null) {
+        $modelo = new $this->modelClass();
+        $modelo->creado = new Expression('now()');
+      } else {
+        $modelo->modificado = new Expression('now()');
+      }
+      /** @var \v1\models\Usuario $modelo */
+
+      $modelo->load($this->req->getBodyParams(), '');
+      if ($pwd !== "") {
+        $modelo->agregarClave($pwd);
+      }
+      if (!$modelo->save()) {
+        return (new Respuesta($modelo))
+          ->mensaje("Hubo un problema al guardar Usuario");
+      }
+
+      $tran->commit();
+      $modelo->refresh();
+
+      return (new Respuesta($modelo))
+        ->mensaje("Usuario guardado con éxito.");
+    } catch (\Exception $e) {
+      $tran->rollBack();
+      return (new Respuesta())
+        ->esError($e)
+        ->mensaje("Hubo un error en el servidor");
+    }
+  }
+
+  public function actionEditar() {
+    $modelo = null;
+    $usuario = $this->usuario;
+
+    if ($usuario) {
+      $modelo = $this->modelClass::findOne($usuario->id);
+    }
+
+    $tran = \YII::$app->getDb()->beginTransaction();
+    if ($modelo === null) {
+      return (new Respuesta($modelo))
+        ->mensaje("No se encontró el usuario.");
+    } else {
+      $modelo->modificado = new Expression('now()');
+    }
+    $modelo->load($this->req->getBodyParams(), '');
+    if (!$modelo->save()) {
+      $tran->rollBack();
+      return (new Respuesta($modelo))
+        ->mensaje("Hubo un problema al guardar Usuario");
+    }
+    $tran->commit();
+    $modelo->refresh();
+    return (new Respuesta($modelo))
+      ->mensaje("Perfil actualizado.");
+  }
+
+  public function actionDesbloquearClave() {
+    $id = intval($this->req->getBodyParam("id", null));
+    $usuario = Usuario::findOne($id);
+
+    $usuario->falloClave = 0;
+    $usuario->ultimoFallo = new Expression('now()');
+
+    if (!$usuario->save())
+      return (new Respuesta())
+        ->esError()
+        ->mensaje("Hubo un error en el servidor");
+
+
+    return (new Respuesta())
+      ->mensaje("Cuenta reestablecida con Éxito");
+  }
+
+  public function actionEliminar() {
+    $id = intval($this->req->getBodyParam("id", null));
+    $modelo = null;
+
+    if ($id > 0) {
+      $modelo = $this->modelClass::findOne(["id" => $id]);
+    }
+    if ($modelo === null) {
+      return (new Respuesta())
+        ->esError()
+        ->mensaje("Usuario no encontrado");
+    }
+    $modelo->eliminado = new Expression('now()');
+    if (!$modelo->save()) {
+      return (new Respuesta($modelo))
+        ->mensaje("No se pudo eliminar Usuario");
+    }
+
+    return (new Respuesta())
+      ->mensaje("Usuario eliminado");
+  }
+
+  public function actionCambiarClave() {
+    $idUsuario =  intval($this->req->getBodyParam("idUsuario", ""));
+    $claveActual = trim($this->req->getBodyParam("claveActual", ""));
+    $clave = trim($this->req->getBodyParam("clave", ""));
+
+    $modelo = Usuario::find()
+      ->andWhere(["id" => $idUsuario])
+      ->one();
+
+    if ($modelo === null) {
+      return (new Respuesta())
+        ->esError()
+        ->mensaje("Ocurrió un error al leer el perfil");
+    }
+    /** @var \v1\models\Usuario $modelo */
+    if (!$modelo->validarClave($claveActual)) {
+      $modelo->addError("clave", "La clave actual no es correcta");
+      return (new Respuesta($modelo));
+    }
+
+    $modelo->agregarClave($clave);
+    if (!$modelo->save()) {
+      return (new Respuesta($modelo))
+        ->mensaje("Ocurrió un error al guardar el perfil");
+    }
+
+    $modelo->refresh();
+    return (new Respuesta($modelo))
+      ->mensaje("Contraseña actualizada");
+  }
+}

+ 34 - 0
modules/v1/models/Media.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace v1\models;
+
+use app\models\Media as ModeloPrograma;
+
+class Media extends ModeloPrograma {
+
+  public function fields() {
+    return [
+      'id',
+      'idUsuario',
+      'nombre',
+      'uuid',
+      'size',
+      'mimetype',
+      'ruta',
+      'descripcion',
+      'creado',
+      'modificado',
+      'eliminado',
+    ];
+  }
+
+  public function extraFields() {
+    return [
+      'usuario'
+    ];
+  }
+
+  public function getUsuario() {
+    return $this->hasOne(Usuario::class, ['id' => 'idUsuario']);
+  }
+}

+ 19 - 0
modules/v1/models/Sesion.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace v1\models;
+
+class Sesion extends \common\models\Usuario {
+
+  public function fields() {
+    return [
+      'id',
+      'correo',
+      'nombre',
+      'estatus',
+      'token' => function($model) {
+        return $model->getAuthKey();
+      }
+    ];
+  }
+
+}

+ 33 - 0
modules/v1/models/Usuario.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace v1\models;
+
+use app\models\Usuario as ModeloUsuario;
+
+class Usuario extends ModeloUsuario {
+
+  public function fields() {
+    return [
+      'id',
+      'correo',
+      'nombre',
+      'estatus',
+      'telefono',
+      'foto',
+      'rol',
+      'login',
+      'creado',
+      'modificado',
+    ];
+  }
+
+  public function extraFields() {
+    return [
+      'media',
+    ];
+  }
+
+  public function getMedia() {
+    return $this->hasMany(Media::class, ['idUsuario' => 'id']);
+  }
+}

+ 162 - 0
requirements.php

@@ -0,0 +1,162 @@
+<?php
+/**
+ * Application requirement checker script.
+ *
+ * In order to run this script use the following console command:
+ * php requirements.php
+ *
+ * In order to run this script from the web, you should copy it to the web root.
+ * If you are using Linux you can create a hard link instead, using the following command:
+ * ln ../requirements.php requirements.php
+ */
+
+// you may need to adjust this path to the correct Yii framework path
+// uncomment and adjust the following line if Yii is not located at the default path
+//$frameworkPath = dirname(__FILE__) . '/vendor/yiisoft/yii2';
+
+
+if (!isset($frameworkPath)) {
+    $searchPaths = array(
+        dirname(__FILE__) . '/vendor/yiisoft/yii2',
+        dirname(__FILE__) . '/../vendor/yiisoft/yii2',
+    );
+    foreach ($searchPaths as $path) {
+        if (is_dir($path)) {
+            $frameworkPath = $path;
+            break;
+        }
+    }
+}
+
+if (!isset($frameworkPath) || !is_dir($frameworkPath)) {
+    $message = "<h1>Error</h1>\n\n"
+        . "<p><strong>The path to yii framework seems to be incorrect.</strong></p>\n"
+        . '<p>You need to install Yii framework via composer or adjust the framework path in file <abbr title="' . __FILE__ . '">' . basename(__FILE__) . "</abbr>.</p>\n"
+        . '<p>Please refer to the <abbr title="' . dirname(__FILE__) . "/README.md\">README</abbr> on how to install Yii.</p>\n";
+
+    if (!empty($_SERVER['argv'])) {
+        // do not print HTML when used in console mode
+        echo strip_tags($message);
+    } else {
+        echo $message;
+    }
+    exit(1);
+}
+
+require_once($frameworkPath . '/requirements/YiiRequirementChecker.php');
+$requirementsChecker = new YiiRequirementChecker();
+
+$gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.';
+$gdOK = $imagickOK = false;
+
+if (extension_loaded('imagick')) {
+    $imagick = new Imagick();
+    $imagickFormats = $imagick->queryFormats('PNG');
+    if (in_array('PNG', $imagickFormats)) {
+        $imagickOK = true;
+    } else {
+        $imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.';
+    }
+}
+
+if (extension_loaded('gd')) {
+    $gdInfo = gd_info();
+    if (!empty($gdInfo['FreeType Support'])) {
+        $gdOK = true;
+    } else {
+        $gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.';
+    }
+}
+
+/**
+ * Adjust requirements according to your application specifics.
+ */
+$requirements = array(
+    // Database :
+    array(
+        'name' => 'PDO extension',
+        'mandatory' => true,
+        'condition' => extension_loaded('pdo'),
+        'by' => 'All DB-related classes',
+    ),
+    array(
+        'name' => 'PDO SQLite extension',
+        'mandatory' => false,
+        'condition' => extension_loaded('pdo_sqlite'),
+        'by' => 'All DB-related classes',
+        'memo' => 'Required for SQLite database.',
+    ),
+    array(
+        'name' => 'PDO MySQL extension',
+        'mandatory' => false,
+        'condition' => extension_loaded('pdo_mysql'),
+        'by' => 'All DB-related classes',
+        'memo' => 'Required for MySQL database.',
+    ),
+    array(
+        'name' => 'PDO PostgreSQL extension',
+        'mandatory' => false,
+        'condition' => extension_loaded('pdo_pgsql'),
+        'by' => 'All DB-related classes',
+        'memo' => 'Required for PostgreSQL database.',
+    ),
+    // Cache :
+    array(
+        'name' => 'Memcache extension',
+        'mandatory' => false,
+        'condition' => extension_loaded('memcache') || extension_loaded('memcached'),
+        'by' => '<a href="http://www.yiiframework.com/doc-2.0/yii-caching-memcache.html">MemCache</a>',
+        'memo' => extension_loaded('memcached') ? 'To use memcached set <a href="http://www.yiiframework.com/doc-2.0/yii-caching-memcache.html#$useMemcached-detail">MemCache::useMemcached</a> to <code>true</code>.' : ''
+    ),
+    // CAPTCHA:
+    array(
+        'name' => 'GD PHP extension with FreeType support',
+        'mandatory' => false,
+        'condition' => $gdOK,
+        'by' => '<a href="http://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
+        'memo' => $gdMemo,
+    ),
+    array(
+        'name' => 'ImageMagick PHP extension with PNG support',
+        'mandatory' => false,
+        'condition' => $imagickOK,
+        'by' => '<a href="http://www.yiiframework.com/doc-2.0/yii-captcha-captcha.html">Captcha</a>',
+        'memo' => $imagickMemo,
+    ),
+    // PHP ini :
+    'phpExposePhp' => array(
+        'name' => 'Expose PHP',
+        'mandatory' => false,
+        'condition' => $requirementsChecker->checkPhpIniOff("expose_php"),
+        'by' => 'Security reasons',
+        'memo' => '"expose_php" should be disabled at php.ini',
+    ),
+    'phpAllowUrlInclude' => array(
+        'name' => 'PHP allow url include',
+        'mandatory' => false,
+        'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"),
+        'by' => 'Security reasons',
+        'memo' => '"allow_url_include" should be disabled at php.ini',
+    ),
+    'phpSmtp' => array(
+        'name' => 'PHP mail SMTP',
+        'mandatory' => false,
+        'condition' => strlen(ini_get('SMTP')) > 0,
+        'by' => 'Email sending',
+        'memo' => 'PHP mail SMTP server required',
+    ),
+);
+
+// OPcache check
+if (!version_compare(phpversion(), '5.5', '>=')) {
+    $requirements[] = array(
+        'name' => 'APC extension',
+        'mandatory' => false,
+        'condition' => extension_loaded('apc'),
+        'by' => '<a href="http://www.yiiframework.com/doc-2.0/yii-caching-apccache.html">ApcCache</a>',
+    );
+}
+
+$result = $requirementsChecker->checkYii()->check($requirements)->getResult();
+$requirementsChecker->render();
+exit($result['summary']['errors'] === 0 ? 0 : 1);

+ 2 - 0
runtime/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 6 - 0
tests/_bootstrap.php

@@ -0,0 +1,6 @@
+<?php
+define('YII_ENV', 'test');
+defined('YII_DEBUG') or define('YII_DEBUG', true);
+
+require_once __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
+require __DIR__ .'/../vendor/autoload.php';

+ 1 - 0
tests/_data/.gitkeep

@@ -0,0 +1 @@
+

+ 2 - 0
tests/_output/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 26 - 0
tests/_support/AcceptanceTester.php

@@ -0,0 +1,26 @@
+<?php
+
+
+/**
+ * Inherited Methods
+ * @method void wantToTest($text)
+ * @method void wantTo($text)
+ * @method void execute($callable)
+ * @method void expectTo($prediction)
+ * @method void expect($prediction)
+ * @method void amGoingTo($argumentation)
+ * @method void am($role)
+ * @method void lookForwardTo($achieveValue)
+ * @method void comment($description)
+ * @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
+ *
+ * @SuppressWarnings(PHPMD)
+*/
+class AcceptanceTester extends \Codeception\Actor
+{
+    use _generated\AcceptanceTesterActions;
+
+   /**
+    * Define custom actions here
+    */
+}

+ 23 - 0
tests/_support/FunctionalTester.php

@@ -0,0 +1,23 @@
+<?php
+
+
+/**
+ * Inherited Methods
+ * @method void wantToTest($text)
+ * @method void wantTo($text)
+ * @method void execute($callable)
+ * @method void expectTo($prediction)
+ * @method void expect($prediction)
+ * @method void amGoingTo($argumentation)
+ * @method void am($role)
+ * @method void lookForwardTo($achieveValue)
+ * @method void comment($description)
+ * @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
+ *
+ * @SuppressWarnings(PHPMD)
+*/
+class FunctionalTester extends \Codeception\Actor
+{
+    use _generated\FunctionalTesterActions;
+
+}

+ 26 - 0
tests/_support/UnitTester.php

@@ -0,0 +1,26 @@
+<?php
+
+
+/**
+ * Inherited Methods
+ * @method void wantToTest($text)
+ * @method void wantTo($text)
+ * @method void execute($callable)
+ * @method void expectTo($prediction)
+ * @method void expect($prediction)
+ * @method void amGoingTo($argumentation)
+ * @method void am($role)
+ * @method void lookForwardTo($achieveValue)
+ * @method void comment($description)
+ * @method \Codeception\Lib\Friend haveFriend($name, $actorClass = NULL)
+ *
+ * @SuppressWarnings(PHPMD)
+*/
+class UnitTester extends \Codeception\Actor
+{
+    use _generated\UnitTesterActions;
+
+   /**
+    * Define custom actions here
+    */
+}

+ 10 - 0
tests/acceptance.suite.yml.example

@@ -0,0 +1,10 @@
+class_name: AcceptanceTester
+modules:
+    enabled:
+        - WebDriver:
+            url: http://127.0.0.1:8080/
+            browser: firefox
+        - Yii2:
+            part: orm
+            entryScript: index-test.php
+            cleanup: false

+ 12 - 0
tests/acceptance/AboutCest.php

@@ -0,0 +1,12 @@
+<?php
+
+use yii\helpers\Url;
+
+class AboutCest
+{
+    public function ensureThatAboutWorks(AcceptanceTester $I)
+    {
+        $I->amOnPage(Url::toRoute('/site/about'));
+        $I->see('About', 'h1');
+    }
+}

+ 34 - 0
tests/acceptance/ContactCest.php

@@ -0,0 +1,34 @@
+<?php
+
+use yii\helpers\Url;
+
+class ContactCest
+{
+    public function _before(\AcceptanceTester $I)
+    {
+        $I->amOnPage(Url::toRoute('/site/contact'));
+    }
+    
+    public function contactPageWorks(AcceptanceTester $I)
+    {
+        $I->wantTo('ensure that contact page works');
+        $I->see('Contact', 'h1');
+    }
+
+    public function contactFormCanBeSubmitted(AcceptanceTester $I)
+    {
+        $I->amGoingTo('submit contact form with correct data');
+        $I->fillField('#contactform-name', 'tester');
+        $I->fillField('#contactform-email', 'tester@example.com');
+        $I->fillField('#contactform-subject', 'test subject');
+        $I->fillField('#contactform-body', 'test content');
+        $I->fillField('#contactform-verifycode', 'testme');
+
+        $I->click('contact-button');
+        
+        $I->wait(2); // wait for button to be clicked
+
+        $I->dontSeeElement('#contact-form');
+        $I->see('Thank you for contacting us. We will respond to you as soon as possible.');
+    }
+}

+ 18 - 0
tests/acceptance/HomeCest.php

@@ -0,0 +1,18 @@
+<?php
+
+use yii\helpers\Url;
+
+class HomeCest
+{
+    public function ensureThatHomePageWorks(AcceptanceTester $I)
+    {
+        $I->amOnPage(Url::toRoute('/site/index'));        
+        $I->see('My Company');
+        
+        $I->seeLink('About');
+        $I->click('About');
+        $I->wait(2); // wait for page to be opened
+        
+        $I->see('This is the About page.');
+    }
+}

+ 21 - 0
tests/acceptance/LoginCest.php

@@ -0,0 +1,21 @@
+<?php
+
+use yii\helpers\Url;
+
+class LoginCest
+{
+    public function ensureThatLoginWorks(AcceptanceTester $I)
+    {
+        $I->amOnPage(Url::toRoute('/site/login'));
+        $I->see('Login', 'h1');
+
+        $I->amGoingTo('try to login with correct credentials');
+        $I->fillField('input[name="LoginForm[username]"]', 'admin');
+        $I->fillField('input[name="LoginForm[password]"]', 'admin');
+        $I->click('login-button');
+        $I->wait(2); // wait for button to be clicked
+
+        $I->expectTo('see user info');
+        $I->see('Logout');
+    }
+}

+ 1 - 0
tests/acceptance/_bootstrap.php

@@ -0,0 +1 @@
+<?php

+ 29 - 0
tests/bin/yii

@@ -0,0 +1,29 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Yii console bootstrap file.
+ *
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+defined('YII_DEBUG') or define('YII_DEBUG', true);
+defined('YII_ENV') or define('YII_ENV', 'test');
+
+require __DIR__ . '/../../vendor/autoload.php';
+require __DIR__ . '/../../vendor/yiisoft/yii2/Yii.php';
+
+$config = yii\helpers\ArrayHelper::merge(
+    require __DIR__ . '/../../config/console.php',
+    [
+        'components' => [
+            'db' => require __DIR__ . '/../../config/test_db.php'
+        ]
+    ]
+);
+
+
+$application = new yii\console\Application($config);
+$exitCode = $application->run();
+exit($exitCode);

+ 20 - 0
tests/bin/yii.bat

@@ -0,0 +1,20 @@
+@echo off
+
+rem -------------------------------------------------------------
+rem  Yii command line bootstrap script for Windows.
+rem
+rem  @author Qiang Xue <qiang.xue@gmail.com>
+rem  @link http://www.yiiframework.com/
+rem  @copyright Copyright (c) 2008 Yii Software LLC
+rem  @license http://www.yiiframework.com/license/
+rem -------------------------------------------------------------
+
+@setlocal
+
+set YII_PATH=%~dp0
+
+if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
+
+"%PHP_COMMAND%" "%YII_PATH%yii" %*
+
+@endlocal

+ 13 - 0
tests/functional.suite.yml

@@ -0,0 +1,13 @@
+# Codeception Test Suite Configuration
+
+# suite for functional (integration) tests.
+# emulate web requests and make application process them.
+# (tip: better to use with frameworks).
+
+# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
+#basic/web/index.php
+class_name: FunctionalTester
+modules:
+    enabled:
+      - Filesystem
+      - Yii2

+ 57 - 0
tests/functional/ContactFormCest.php

@@ -0,0 +1,57 @@
+<?php
+
+class ContactFormCest 
+{
+    public function _before(\FunctionalTester $I)
+    {
+        $I->amOnPage(['site/contact']);
+    }
+
+    public function openContactPage(\FunctionalTester $I)
+    {
+        $I->see('Contact', 'h1');        
+    }
+
+    public function submitEmptyForm(\FunctionalTester $I)
+    {
+        $I->submitForm('#contact-form', []);
+        $I->expectTo('see validations errors');
+        $I->see('Contact', 'h1');
+        $I->see('Name cannot be blank');
+        $I->see('Email cannot be blank');
+        $I->see('Subject cannot be blank');
+        $I->see('Body cannot be blank');
+        $I->see('The verification code is incorrect');
+    }
+
+    public function submitFormWithIncorrectEmail(\FunctionalTester $I)
+    {
+        $I->submitForm('#contact-form', [
+            'ContactForm[name]' => 'tester',
+            'ContactForm[email]' => 'tester.email',
+            'ContactForm[subject]' => 'test subject',
+            'ContactForm[body]' => 'test content',
+            'ContactForm[verifyCode]' => 'testme',
+        ]);
+        $I->expectTo('see that email address is wrong');
+        $I->dontSee('Name cannot be blank', '.help-inline');
+        $I->see('Email is not a valid email address.');
+        $I->dontSee('Subject cannot be blank', '.help-inline');
+        $I->dontSee('Body cannot be blank', '.help-inline');
+        $I->dontSee('The verification code is incorrect', '.help-inline');        
+    }
+
+    public function submitFormSuccessfully(\FunctionalTester $I)
+    {
+        $I->submitForm('#contact-form', [
+            'ContactForm[name]' => 'tester',
+            'ContactForm[email]' => 'tester@example.com',
+            'ContactForm[subject]' => 'test subject',
+            'ContactForm[body]' => 'test content',
+            'ContactForm[verifyCode]' => 'testme',
+        ]);
+        $I->seeEmailIsSent();
+        $I->dontSeeElement('#contact-form');
+        $I->see('Thank you for contacting us. We will respond to you as soon as possible.');        
+    }
+}

+ 59 - 0
tests/functional/LoginFormCest.php

@@ -0,0 +1,59 @@
+<?php
+
+class LoginFormCest
+{
+    public function _before(\FunctionalTester $I)
+    {
+        $I->amOnRoute('site/login');
+    }
+
+    public function openLoginPage(\FunctionalTester $I)
+    {
+        $I->see('Login', 'h1');
+
+    }
+
+    // demonstrates `amLoggedInAs` method
+    public function internalLoginById(\FunctionalTester $I)
+    {
+        $I->amLoggedInAs(100);
+        $I->amOnPage('/');
+        $I->see('Logout (admin)');
+    }
+
+    // demonstrates `amLoggedInAs` method
+    public function internalLoginByInstance(\FunctionalTester $I)
+    {
+        $I->amLoggedInAs(\app\models\User::findByUsername('admin'));
+        $I->amOnPage('/');
+        $I->see('Logout (admin)');
+    }
+
+    public function loginWithEmptyCredentials(\FunctionalTester $I)
+    {
+        $I->submitForm('#login-form', []);
+        $I->expectTo('see validations errors');
+        $I->see('Username cannot be blank.');
+        $I->see('Password cannot be blank.');
+    }
+
+    public function loginWithWrongCredentials(\FunctionalTester $I)
+    {
+        $I->submitForm('#login-form', [
+            'LoginForm[username]' => 'admin',
+            'LoginForm[password]' => 'wrong',
+        ]);
+        $I->expectTo('see validations errors');
+        $I->see('Incorrect username or password.');
+    }
+
+    public function loginSuccessfully(\FunctionalTester $I)
+    {
+        $I->submitForm('#login-form', [
+            'LoginForm[username]' => 'admin',
+            'LoginForm[password]' => 'admin',
+        ]);
+        $I->see('Logout (admin)');
+        $I->dontSeeElement('form#login-form');              
+    }
+}

+ 1 - 0
tests/functional/_bootstrap.php

@@ -0,0 +1 @@
+<?php

+ 11 - 0
tests/unit.suite.yml

@@ -0,0 +1,11 @@
+# Codeception Test Suite Configuration
+
+# suite for unit (internal) tests.
+# RUN `build` COMMAND AFTER ADDING/REMOVING MODULES.
+
+class_name: UnitTester
+modules:
+    enabled:
+      - Asserts
+      - Yii2:
+            part: [orm, email, fixtures]

+ 3 - 0
tests/unit/_bootstrap.php

@@ -0,0 +1,3 @@
+<?php
+
+// add unit testing specific bootstrap code here

+ 41 - 0
tests/unit/models/ContactFormTest.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace tests\unit\models;
+
+use app\models\ContactForm;
+use yii\mail\MessageInterface;
+
+class ContactFormTest extends \Codeception\Test\Unit
+{
+    /**
+     * @var \UnitTester
+     */
+    public $tester;
+
+    public function testEmailIsSentOnContact()
+    {
+        $model = new ContactForm();
+
+        $model->attributes = [
+            'name' => 'Tester',
+            'email' => 'tester@example.com',
+            'subject' => 'very important letter subject',
+            'body' => 'body of current message',
+            'verifyCode' => 'testme',
+        ];
+
+        expect_that($model->contact('admin@example.com'));
+
+        // using Yii2 module actions to check email was sent
+        $this->tester->seeEmailIsSent();
+
+        /** @var MessageInterface $emailMessage */
+        $emailMessage = $this->tester->grabLastSentEmail();
+        expect('valid email is sent', $emailMessage)->isInstanceOf('yii\mail\MessageInterface');
+        expect($emailMessage->getTo())->hasKey('admin@example.com');
+        expect($emailMessage->getFrom())->hasKey('noreply@example.com');
+        expect($emailMessage->getReplyTo())->hasKey('tester@example.com');
+        expect($emailMessage->getSubject())->equals('very important letter subject');
+        expect($emailMessage->toString())->stringContainsString('body of current message');
+    }
+}

+ 51 - 0
tests/unit/models/LoginFormTest.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace tests\unit\models;
+
+use app\models\LoginForm;
+
+class LoginFormTest extends \Codeception\Test\Unit
+{
+    private $model;
+
+    protected function _after()
+    {
+        \Yii::$app->user->logout();
+    }
+
+    public function testLoginNoUser()
+    {
+        $this->model = new LoginForm([
+            'username' => 'not_existing_username',
+            'password' => 'not_existing_password',
+        ]);
+
+        expect_not($this->model->login());
+        expect_that(\Yii::$app->user->isGuest);
+    }
+
+    public function testLoginWrongPassword()
+    {
+        $this->model = new LoginForm([
+            'username' => 'demo',
+            'password' => 'wrong_password',
+        ]);
+
+        expect_not($this->model->login());
+        expect_that(\Yii::$app->user->isGuest);
+        expect($this->model->errors)->hasKey('password');
+    }
+
+    public function testLoginCorrect()
+    {
+        $this->model = new LoginForm([
+            'username' => 'demo',
+            'password' => 'demo',
+        ]);
+
+        expect_that($this->model->login());
+        expect_not(\Yii::$app->user->isGuest);
+        expect($this->model->errors)->hasntKey('password');
+    }
+
+}

+ 44 - 0
tests/unit/models/UserTest.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace tests\unit\models;
+
+use app\models\User;
+
+class UserTest extends \Codeception\Test\Unit
+{
+    public function testFindUserById()
+    {
+        expect_that($user = User::findIdentity(100));
+        expect($user->username)->equals('admin');
+
+        expect_not(User::findIdentity(999));
+    }
+
+    public function testFindUserByAccessToken()
+    {
+        expect_that($user = User::findIdentityByAccessToken('100-token'));
+        expect($user->username)->equals('admin');
+
+        expect_not(User::findIdentityByAccessToken('non-existing'));        
+    }
+
+    public function testFindUserByUsername()
+    {
+        expect_that($user = User::findByUsername('admin'));
+        expect_not(User::findByUsername('not-admin'));
+    }
+
+    /**
+     * @depends testFindUserByUsername
+     */
+    public function testValidateUser($user)
+    {
+        $user = User::findByUsername('admin');
+        expect_that($user->validateAuthKey('test100key'));
+        expect_not($user->validateAuthKey('test102key'));
+
+        expect_that($user->validatePassword('admin'));
+        expect_not($user->validatePassword('123456'));        
+    }
+
+}

+ 261 - 0
tests/unit/widgets/AlertTest.php

@@ -0,0 +1,261 @@
+<?php
+
+namespace tests\unit\widgets;
+
+use app\widgets\Alert;
+use Yii;
+
+class AlertTest extends \Codeception\Test\Unit
+{
+    public function testSingleErrorMessage()
+    {
+        $message = 'This is an error message';
+
+        Yii::$app->session->setFlash('error', $message);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($message);
+        expect($renderingResult)->stringContainsString('alert-danger');
+
+        expect($renderingResult)->stringNotContainsString('alert-success');
+        expect($renderingResult)->stringNotContainsString('alert-info');
+        expect($renderingResult)->stringNotContainsString('alert-warning');
+    }
+
+    public function testMultipleErrorMessages()
+    {
+        $firstMessage = 'This is the first error message';
+        $secondMessage = 'This is the second error message';
+
+        Yii::$app->session->setFlash('error', [$firstMessage, $secondMessage]);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($firstMessage);
+        expect($renderingResult)->stringContainsString($secondMessage);
+        expect($renderingResult)->stringContainsString('alert-danger');
+
+        expect($renderingResult)->stringNotContainsString('alert-success');
+        expect($renderingResult)->stringNotContainsString('alert-info');
+        expect($renderingResult)->stringNotContainsString('alert-warning');
+    }
+
+    public function testSingleDangerMessage()
+    {
+        $message = 'This is a danger message';
+
+        Yii::$app->session->setFlash('danger', $message);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($message);
+        expect($renderingResult)->stringContainsString('alert-danger');
+
+        expect($renderingResult)->stringNotContainsString('alert-success');
+        expect($renderingResult)->stringNotContainsString('alert-info');
+        expect($renderingResult)->stringNotContainsString('alert-warning');
+    }
+
+    public function testMultipleDangerMessages()
+    {
+        $firstMessage = 'This is the first danger message';
+        $secondMessage = 'This is the second danger message';
+
+        Yii::$app->session->setFlash('danger', [$firstMessage, $secondMessage]);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($firstMessage);
+        expect($renderingResult)->stringContainsString($secondMessage);
+        expect($renderingResult)->stringContainsString('alert-danger');
+
+        expect($renderingResult)->stringNotContainsString('alert-success');
+        expect($renderingResult)->stringNotContainsString('alert-info');
+        expect($renderingResult)->stringNotContainsString('alert-warning');
+    }
+
+    public function testSingleSuccessMessage()
+    {
+        $message = 'This is a success message';
+
+        Yii::$app->session->setFlash('success', $message);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($message);
+        expect($renderingResult)->stringContainsString('alert-success');
+
+        expect($renderingResult)->stringNotContainsString('alert-danger');
+        expect($renderingResult)->stringNotContainsString('alert-info');
+        expect($renderingResult)->stringNotContainsString('alert-warning');
+    }
+
+    public function testMultipleSuccessMessages()
+    {
+        $firstMessage = 'This is the first danger message';
+        $secondMessage = 'This is the second danger message';
+
+        Yii::$app->session->setFlash('success', [$firstMessage, $secondMessage]);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($firstMessage);
+        expect($renderingResult)->stringContainsString($secondMessage);
+        expect($renderingResult)->stringContainsString('alert-success');
+
+        expect($renderingResult)->stringNotContainsString('alert-danger');
+        expect($renderingResult)->stringNotContainsString('alert-info');
+        expect($renderingResult)->stringNotContainsString('alert-warning');
+    }
+
+    public function testSingleInfoMessage()
+    {
+        $message = 'This is an info message';
+
+        Yii::$app->session->setFlash('info', $message);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($message);
+        expect($renderingResult)->stringContainsString('alert-info');
+
+        expect($renderingResult)->stringNotContainsString('alert-danger');
+        expect($renderingResult)->stringNotContainsString('alert-success');
+        expect($renderingResult)->stringNotContainsString('alert-warning');
+    }
+
+    public function testMultipleInfoMessages()
+    {
+        $firstMessage = 'This is the first info message';
+        $secondMessage = 'This is the second info message';
+
+        Yii::$app->session->setFlash('info', [$firstMessage, $secondMessage]);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($firstMessage);
+        expect($renderingResult)->stringContainsString($secondMessage);
+        expect($renderingResult)->stringContainsString('alert-info');
+
+        expect($renderingResult)->stringNotContainsString('alert-danger');
+        expect($renderingResult)->stringNotContainsString('alert-success');
+        expect($renderingResult)->stringNotContainsString('alert-warning');
+    }
+
+    public function testSingleWarningMessage()
+    {
+        $message = 'This is a warning message';
+
+        Yii::$app->session->setFlash('warning', $message);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($message);
+        expect($renderingResult)->stringContainsString('alert-warning');
+
+        expect($renderingResult)->stringNotContainsString('alert-danger');
+        expect($renderingResult)->stringNotContainsString('alert-success');
+        expect($renderingResult)->stringNotContainsString('alert-info');
+    }
+
+    public function testMultipleWarningMessages()
+    {
+        $firstMessage = 'This is the first warning message';
+        $secondMessage = 'This is the second warning message';
+
+        Yii::$app->session->setFlash('warning', [$firstMessage, $secondMessage]);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($firstMessage);
+        expect($renderingResult)->stringContainsString($secondMessage);
+        expect($renderingResult)->stringContainsString('alert-warning');
+
+        expect($renderingResult)->stringNotContainsString('alert-danger');
+        expect($renderingResult)->stringNotContainsString('alert-success');
+        expect($renderingResult)->stringNotContainsString('alert-info');
+    }
+
+    public function testSingleMixedMessages() {
+        $errorMessage = 'This is an error message';
+        $dangerMessage = 'This is a danger message';
+        $successMessage = 'This is a success message';
+        $infoMessage = 'This is a info message';
+        $warningMessage = 'This is a warning message';
+
+        Yii::$app->session->setFlash('error', $errorMessage);
+        Yii::$app->session->setFlash('danger', $dangerMessage);
+        Yii::$app->session->setFlash('success', $successMessage);
+        Yii::$app->session->setFlash('info', $infoMessage);
+        Yii::$app->session->setFlash('warning', $warningMessage);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($errorMessage);
+        expect($renderingResult)->stringContainsString($dangerMessage);
+        expect($renderingResult)->stringContainsString($successMessage);
+        expect($renderingResult)->stringContainsString($infoMessage);
+        expect($renderingResult)->stringContainsString($warningMessage);
+
+        expect($renderingResult)->stringContainsString('alert-danger');
+        expect($renderingResult)->stringContainsString('alert-success');
+        expect($renderingResult)->stringContainsString('alert-info');
+        expect($renderingResult)->stringContainsString('alert-warning');
+    }
+
+    public function testMultipleMixedMessages() {
+        $firstErrorMessage = 'This is the first error message';
+        $secondErrorMessage = 'This is the second error message';
+        $firstDangerMessage = 'This is the first danger message';
+        $secondDangerMessage = 'This is the second';
+        $firstSuccessMessage = 'This is the first success message';
+        $secondSuccessMessage = 'This is the second success message';
+        $firstInfoMessage = 'This is the first info message';
+        $secondInfoMessage = 'This is the second info message';
+        $firstWarningMessage = 'This is the first warning message';
+        $secondWarningMessage = 'This is the second warning message';
+
+        Yii::$app->session->setFlash('error', [$firstErrorMessage, $secondErrorMessage]);
+        Yii::$app->session->setFlash('danger', [$firstDangerMessage, $secondDangerMessage]);
+        Yii::$app->session->setFlash('success', [$firstSuccessMessage, $secondSuccessMessage]);
+        Yii::$app->session->setFlash('info', [$firstInfoMessage, $secondInfoMessage]);
+        Yii::$app->session->setFlash('warning', [$firstWarningMessage, $secondWarningMessage]);
+
+        $renderingResult = Alert::widget();
+
+        expect($renderingResult)->stringContainsString($firstErrorMessage);
+        expect($renderingResult)->stringContainsString($secondErrorMessage);
+        expect($renderingResult)->stringContainsString($firstDangerMessage);
+        expect($renderingResult)->stringContainsString($secondDangerMessage);
+        expect($renderingResult)->stringContainsString($firstSuccessMessage);
+        expect($renderingResult)->stringContainsString($secondSuccessMessage);
+        expect($renderingResult)->stringContainsString($firstInfoMessage);
+        expect($renderingResult)->stringContainsString($secondInfoMessage);
+        expect($renderingResult)->stringContainsString($firstWarningMessage);
+        expect($renderingResult)->stringContainsString($secondWarningMessage);
+
+        expect($renderingResult)->stringContainsString('alert-danger');
+        expect($renderingResult)->stringContainsString('alert-success');
+        expect($renderingResult)->stringContainsString('alert-info');
+        expect($renderingResult)->stringContainsString('alert-warning');
+    }
+
+    public function testFlashIntegrity()
+    {
+        $errorMessage = 'This is an error message';
+        $unrelatedMessage = 'This is a message that is not related to the alert widget';
+
+        Yii::$app->session->setFlash('error', $errorMessage);
+        Yii::$app->session->setFlash('unrelated', $unrelatedMessage);
+
+        Alert::widget();
+
+        // Simulate redirect
+        Yii::$app->session->close();
+        Yii::$app->session->open();
+
+        expect(Yii::$app->session->getFlash('error'))->null();
+        expect(Yii::$app->session->getFlash('unrelated'))->equals($unrelatedMessage);
+    }
+}

+ 2 - 0
vagrant/config/.gitignore

@@ -0,0 +1,2 @@
+# local configuration
+vagrant-local.yml

+ 22 - 0
vagrant/config/vagrant-local.example.yml

@@ -0,0 +1,22 @@
+# Your personal GitHub token
+github_token: <your-personal-github-token>
+# Read more: https://github.com/blog/1509-personal-api-tokens
+# You can generate it here: https://github.com/settings/tokens
+
+# Guest OS timezone
+timezone: Europe/London
+
+# Are we need check box updates for every 'vagrant up'?
+box_check_update: false
+
+# Virtual machine name
+machine_name: yii2basic
+
+# Virtual machine IP
+ip: 192.168.83.137
+
+# Virtual machine CPU cores number
+cpus: 1
+
+# Virtual machine RAM
+memory: 1024

+ 38 - 0
vagrant/nginx/app.conf

@@ -0,0 +1,38 @@
+server {
+   charset utf-8;
+   client_max_body_size 128M;
+   sendfile off;
+
+   listen 80; ## listen for ipv4
+   #listen [::]:80 default_server ipv6only=on; ## listen for ipv6
+
+   server_name yii2basic.test;
+   root        /app/web/;
+   index       index.php;
+
+   access_log  /app/vagrant/nginx/log/yii2basic.access.log;
+   error_log   /app/vagrant/nginx/log/yii2basic.error.log;
+
+   location / {
+       # Redirect everything that isn't a real file to index.php
+       try_files $uri $uri/ /index.php$is_args$args;
+   }
+
+   # uncomment to avoid processing of calls to non-existing static files by Yii
+   #location ~ \.(js|css|png|jpg|gif|swf|ico|pdf|mov|fla|zip|rar)$ {
+   #    try_files $uri =404;
+   #}
+   #error_page 404 /404.html;
+
+   location ~ \.php$ {
+       include fastcgi_params;
+       fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+       #fastcgi_pass   127.0.0.1:9000;
+       fastcgi_pass unix:/var/run/php/php7.2-fpm.sock;
+       try_files $uri =404;
+   }
+
+   location ~ /\.(ht|svn|git) {
+       deny all;
+   }
+}

+ 3 - 0
vagrant/nginx/log/.gitignore

@@ -0,0 +1,3 @@
+#nginx logs
+yii2basic.access.log
+yii2basic.error.log

+ 18 - 0
vagrant/provision/always-as-root.sh

@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+
+#== Bash helpers ==
+
+function info {
+  echo " "
+  echo "--> $1"
+  echo " "
+}
+
+#== Provision script ==
+
+info "Provision-script user: `whoami`"
+
+info "Restart web-stack"
+service php7.2-fpm restart
+service nginx restart
+service mysql restart

+ 79 - 0
vagrant/provision/once-as-root.sh

@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+
+#== Import script args ==
+
+timezone=$(echo "$1")
+readonly IP=$2
+
+#== Bash helpers ==
+
+function info {
+  echo " "
+  echo "--> $1"
+  echo " "
+}
+
+#== Provision script ==
+
+info "Provision-script user: `whoami`"
+
+export DEBIAN_FRONTEND=noninteractive
+
+info "Configure timezone"
+timedatectl set-timezone ${timezone} --no-ask-password
+
+info "Add the VM IP to the list of allowed IPs"
+awk -v ip=$IP -f /app/vagrant/provision/provision.awk /app/config/web.php
+
+info "Prepare root password for MySQL"
+debconf-set-selections <<< 'mariadb-server mysql-server/root_password password'
+debconf-set-selections <<< 'mariadb-server mysql-server/root_password_again password'
+echo "Done!"
+
+info "Update OS software"
+apt-get update
+apt-get upgrade -y
+
+info "Install additional software"
+apt-get install -y php7.2-curl php7.2-cli php7.2-intl php7.2-mysqlnd php7.2-gd php7.2-fpm php7.2-mbstring php7.2-xml unzip nginx mariadb-server-10.1 php.xdebug
+
+info "Configure MySQL"
+sed -i 's/.*bind-address.*/bind-address = 0.0.0.0/' /etc/mysql/mariadb.conf.d/50-server.cnf
+mysql <<< "CREATE USER 'root'@'%' IDENTIFIED BY ''"
+mysql <<< "GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'"
+mysql <<< "DROP USER 'root'@'localhost'"
+mysql <<< 'FLUSH PRIVILEGES'
+echo "Done!"
+
+info "Configure PHP-FPM"
+sed -i 's/user = www-data/user = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
+sed -i 's/group = www-data/group = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
+sed -i 's/owner = www-data/owner = vagrant/g' /etc/php/7.2/fpm/pool.d/www.conf
+cat << EOF > /etc/php/7.2/mods-available/xdebug.ini
+zend_extension=xdebug.so
+xdebug.remote_enable=1
+xdebug.remote_connect_back=1
+xdebug.remote_port=9000
+xdebug.remote_autostart=1
+EOF
+echo "Done!"
+
+info "Configure NGINX"
+sed -i 's/user www-data/user vagrant/g' /etc/nginx/nginx.conf
+echo "Done!"
+
+info "Enabling site configuration"
+ln -s /app/vagrant/nginx/app.conf /etc/nginx/sites-enabled/app.conf
+echo "Done!"
+
+info "Removing default site configuration"
+rm /etc/nginx/sites-enabled/default
+echo "Done!"
+
+info "Initialize databases for MySQL"
+mysql <<< 'CREATE DATABASE yii2basic'
+mysql <<< 'CREATE DATABASE yii2basic_test'
+echo "Done!"
+
+info "Install composer"
+curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

+ 31 - 0
vagrant/provision/once-as-vagrant.sh

@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+
+#== Import script args ==
+
+github_token=$(echo "$1")
+
+#== Bash helpers ==
+
+function info {
+  echo " "
+  echo "--> $1"
+  echo " "
+}
+
+#== Provision script ==
+
+info "Provision-script user: `whoami`"
+
+info "Configure composer"
+composer config --global github-oauth.github.com ${github_token}
+echo "Done!"
+
+info "Install project dependencies"
+cd /app
+composer --no-progress --prefer-dist install
+
+info "Create bash-alias 'app' for vagrant user"
+echo 'alias app="cd /app"' | tee /home/vagrant/.bash_aliases
+
+info "Enabling colorized prompt for guest console"
+sed -i "s/#force_color_prompt=yes/force_color_prompt=yes/" /home/vagrant/.bashrc

+ 50 - 0
vagrant/provision/provision.awk

@@ -0,0 +1,50 @@
+###
+# Modifying Yii2's files for Vagrant VM
+#
+# @author HA3IK <golubha3ik@gmail.com>
+# @version 1.0.0
+
+BEGIN {
+    print "AWK BEGINs its work:"
+    IGNORECASE = 1
+
+    # Correct IP - wildcard last octet
+    match(ip, /(([0-9]+\.)+)/, arr)
+    ip = arr[1] "*"
+}
+# BODY
+{
+    # Check if it's the same file
+    if (FILENAME != isFile["same"]){
+        msg = "- Work with: " FILENAME
+        # Close a previous file
+        close(isFile["same"])
+        # Delete previous data
+        delete isFile
+        # Save current file
+        isFile["same"] = FILENAME
+        # Define array index for the file
+        switch (FILENAME){
+        case /config\/web\.php$/:
+            isFile["IsConfWeb"] = 1
+            msg = msg " - add allowed IP: " ip
+            break
+        }
+        # Print the concatenated message for the file
+        print msg
+    }
+
+    # IF config/web.php
+    if (isFile["IsConfWeb"]){
+        # IF line has "allowedIPs" and doesn't has our IP
+        if (match($0, "allowedIPs") && !match($0, ip)){
+            match($0, /([^\]]+)(.+)/, arr)
+            $0 = sprintf("%s, '%s'%s", arr[1], ip, arr[2])
+        }
+        # Rewrite the file
+        print $0 > FILENAME
+    }
+}
+END {
+    print "AWK ENDs its work."
+}

+ 28 - 0
views/layouts/main.php

@@ -0,0 +1,28 @@
+<?php
+
+use yii\bootstrap4\Html;
+
+?>
+<?php $this->beginPage() ?>
+<!DOCTYPE html>
+<html lang="<?= Yii::$app->language ?>" class="h-100">
+<head>
+    <meta charset="<?= Yii::$app->charset ?>">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <?php $this->registerCsrfMetaTags() ?>
+    <title><?= Html::encode($this->title) ?></title>
+    <?php $this->head() ?>
+</head>
+<body class="d-flex flex-column h-100">
+<?php $this->beginBody() ?>
+
+<main role="main" class="flex-shrink-0">
+    <div class="container">
+        <?= $content ?>
+    </div>
+</main>
+
+<?php $this->endBody() ?>
+</body>
+</html>
+<?php $this->endPage() ?>

+ 18 - 0
views/site/about.php

@@ -0,0 +1,18 @@
+<?php
+
+/** @var yii\web\View $this */
+
+use yii\helpers\Html;
+
+$this->title = 'About';
+$this->params['breadcrumbs'][] = $this->title;
+?>
+<div class="site-about">
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <p>
+        This is the About page. You may modify the following file to customize its content:
+    </p>
+
+    <code><?= __FILE__ ?></code>
+</div>

+ 68 - 0
views/site/contact.php

@@ -0,0 +1,68 @@
+<?php
+
+/** @var yii\web\View $this */
+/** @var yii\bootstrap4\ActiveForm $form */
+/** @var app\models\ContactForm $model */
+
+use yii\bootstrap4\ActiveForm;
+use yii\bootstrap4\Html;
+use yii\captcha\Captcha;
+
+$this->title = 'Contact';
+$this->params['breadcrumbs'][] = $this->title;
+?>
+<div class="site-contact">
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <?php if (Yii::$app->session->hasFlash('contactFormSubmitted')): ?>
+
+        <div class="alert alert-success">
+            Thank you for contacting us. We will respond to you as soon as possible.
+        </div>
+
+        <p>
+            Note that if you turn on the Yii debugger, you should be able
+            to view the mail message on the mail panel of the debugger.
+            <?php if (Yii::$app->mailer->useFileTransport): ?>
+                Because the application is in development mode, the email is not sent but saved as
+                a file under <code><?= Yii::getAlias(Yii::$app->mailer->fileTransportPath) ?></code>.
+                Please configure the <code>useFileTransport</code> property of the <code>mail</code>
+                application component to be false to enable email sending.
+            <?php endif; ?>
+        </p>
+
+    <?php else: ?>
+
+        <p>
+            If you have business inquiries or other questions, please fill out the following form to contact us.
+            Thank you.
+        </p>
+
+        <div class="row">
+            <div class="col-lg-5">
+
+                <?php $form = ActiveForm::begin(['id' => 'contact-form']); ?>
+
+                    <?= $form->field($model, 'name')->textInput(['autofocus' => true]) ?>
+
+                    <?= $form->field($model, 'email') ?>
+
+                    <?= $form->field($model, 'subject') ?>
+
+                    <?= $form->field($model, 'body')->textarea(['rows' => 6]) ?>
+
+                    <?= $form->field($model, 'verifyCode')->widget(Captcha::className(), [
+                        'template' => '<div class="row"><div class="col-lg-3">{image}</div><div class="col-lg-6">{input}</div></div>',
+                    ]) ?>
+
+                    <div class="form-group">
+                        <?= Html::submitButton('Submit', ['class' => 'btn btn-primary', 'name' => 'contact-button']) ?>
+                    </div>
+
+                <?php ActiveForm::end(); ?>
+
+            </div>
+        </div>
+
+    <?php endif; ?>
+</div>

+ 27 - 0
views/site/error.php

@@ -0,0 +1,27 @@
+<?php
+
+/** @var yii\web\View $this */
+/** @var string $name */
+/** @var string $message */
+/** @var Exception$exception */
+
+use yii\helpers\Html;
+
+$this->title = $name;
+?>
+<div class="site-error">
+
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <div class="alert alert-danger">
+        <?= nl2br(Html::encode($message)) ?>
+    </div>
+
+    <p>
+        The above error occurred while the Web server was processing your request.
+    </p>
+    <p>
+        Please contact us if you think this is a server error. Thank you.
+    </p>
+
+</div>

+ 53 - 0
views/site/index.php

@@ -0,0 +1,53 @@
+<?php
+
+/** @var yii\web\View $this */
+
+$this->title = 'My Yii Application';
+?>
+<div class="site-index">
+
+    <div class="jumbotron text-center bg-transparent">
+        <h1 class="display-4">Congratulations!</h1>
+
+        <p class="lead">You have successfully created your Yii-powered application.</p>
+
+        <p><a class="btn btn-lg btn-success" href="http://www.yiiframework.com">Get started with Yii</a></p>
+    </div>
+
+    <div class="body-content">
+
+        <div class="row">
+            <div class="col-lg-4">
+                <h2>Heading</h2>
+
+                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
+                    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
+                    ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
+                    fugiat nulla pariatur.</p>
+
+                <p><a class="btn btn-outline-secondary" href="http://www.yiiframework.com/doc/">Yii Documentation &raquo;</a></p>
+            </div>
+            <div class="col-lg-4">
+                <h2>Heading</h2>
+
+                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
+                    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
+                    ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
+                    fugiat nulla pariatur.</p>
+
+                <p><a class="btn btn-outline-secondary" href="http://www.yiiframework.com/forum/">Yii Forum &raquo;</a></p>
+            </div>
+            <div class="col-lg-4">
+                <h2>Heading</h2>
+
+                <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et
+                    dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
+                    ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
+                    fugiat nulla pariatur.</p>
+
+                <p><a class="btn btn-outline-secondary" href="http://www.yiiframework.com/extensions/">Yii Extensions &raquo;</a></p>
+            </div>
+        </div>
+
+    </div>
+</div>

+ 49 - 0
views/site/login.php

@@ -0,0 +1,49 @@
+<?php
+
+/** @var yii\web\View $this */
+/** @var yii\bootstrap4\ActiveForm $form */
+/** @var app\models\LoginForm $model */
+
+use yii\bootstrap4\ActiveForm;
+use yii\bootstrap4\Html;
+
+$this->title = 'Login';
+$this->params['breadcrumbs'][] = $this->title;
+?>
+<div class="site-login">
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <p>Please fill out the following fields to login:</p>
+
+    <?php $form = ActiveForm::begin([
+        'id' => 'login-form',
+        'layout' => 'horizontal',
+        'fieldConfig' => [
+            'template' => "{label}\n{input}\n{error}",
+            'labelOptions' => ['class' => 'col-lg-1 col-form-label mr-lg-3'],
+            'inputOptions' => ['class' => 'col-lg-3 form-control'],
+            'errorOptions' => ['class' => 'col-lg-7 invalid-feedback'],
+        ],
+    ]); ?>
+
+        <?= $form->field($model, 'username')->textInput(['autofocus' => true]) ?>
+
+        <?= $form->field($model, 'password')->passwordInput() ?>
+
+        <?= $form->field($model, 'rememberMe')->checkbox([
+            'template' => "<div class=\"offset-lg-1 col-lg-3 custom-control custom-checkbox\">{input} {label}</div>\n<div class=\"col-lg-8\">{error}</div>",
+        ]) ?>
+
+        <div class="form-group">
+            <div class="offset-lg-1 col-lg-11">
+                <?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
+            </div>
+        </div>
+
+    <?php ActiveForm::end(); ?>
+
+    <div class="offset-lg-1" style="color:#999;">
+        You may login with <strong>admin/admin</strong> or <strong>demo/demo</strong>.<br>
+        To modify the username/password, please check out the code <code>app\models\User::$users</code>.
+    </div>
+</div>

+ 4 - 0
web/.htaccess

@@ -0,0 +1,4 @@
+RewriteEngine on
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteRule . index.php [L]

+ 2 - 0
web/assets/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 54 - 0
web/css/pdf.css

@@ -0,0 +1,54 @@
+.title-container{
+	margin-bottom: 20px;
+}
+
+.table-tittle{
+	border-bottom: 1px solid #001529;
+	text-align: center;
+	margin-left: auto;
+	margin-right: auto;
+	width: 80%;
+}
+
+.tittle{
+	font-size: 30px;
+	font-weight: bold;
+	margin-bottom: 5px;
+}
+
+.info-container{
+	margin-left: auto;
+	margin-right: auto;
+	margin-bottom: 20px;
+	width: 80%;
+}
+
+.info-table{
+	width: 100%;
+}
+
+.proyecto-etapa{
+	font-weight: bold;
+	font-size: 25px;
+}
+
+.actividad-usuario{
+	font-size: 20px;
+}
+
+.task-table{
+	border: 1px solid black;
+	border-collapse: collapse;
+}
+
+.task-table th{
+	background-color: #1677ff;
+	border: 1px solid #001529;
+	color: white;
+}
+
+.task-table td{
+	border: 1px solid #001529;
+	margin: 5px;
+	padding: 5px;
+}

+ 84 - 0
web/css/site.css

@@ -0,0 +1,84 @@
+main > .container {
+    padding: 70px 15px 20px;
+}
+
+.footer {
+    background-color: #f5f5f5;
+    font-size: .9em;
+    height: 60px;
+}
+
+.footer > .container {
+    padding-right: 15px;
+    padding-left: 15px;
+}
+
+.not-set {
+    color: #c55;
+    font-style: italic;
+}
+
+/* add sorting icons to gridview sort links */
+a.asc:after, a.desc:after {
+    content: '';
+    left: 3px;
+    display: inline-block;
+    width: 0;
+    height: 0;
+    border: solid 5px transparent;
+    margin: 4px 4px 2px 4px;
+    background: transparent;
+}
+
+a.asc:after {
+    border-bottom: solid 7px #212529;
+    border-top-width: 0;
+}
+
+a.desc:after {
+    border-top: solid 7px #212529;
+    border-bottom-width: 0;
+}
+
+.grid-view th {
+    white-space: nowrap;
+}
+
+.hint-block {
+    display: block;
+    margin-top: 5px;
+    color: #999;
+}
+
+.error-summary {
+    color: #a94442;
+    background: #fdf7f7;
+    border-left: 3px solid #eed3d7;
+    padding: 10px 20px;
+    margin: 0 0 15px 0;
+}
+
+/* align the logout "link" (button in form) of the navbar */
+.nav li > form > button.logout {
+    padding-top: 7px;
+    color: rgba(255, 255, 255, 0.5);
+}
+
+@media(max-width:767px) {
+    .nav li > form > button.logout {
+        display:block;
+        text-align: left;
+        width: 100%;
+        padding: 10px 0;
+    }
+}
+
+.nav > li > form > button.logout:focus,
+.nav > li > form > button.logout:hover {
+    text-decoration: none;
+    color: rgba(255, 255, 255, 0.75);
+}
+
+.nav > li > form > button.logout:focus {
+    outline: none;
+}

BIN
web/favicon.ico


+ 16 - 0
web/index-test.php

@@ -0,0 +1,16 @@
+<?php
+
+// NOTE: Make sure this file is not accessible when deployed to production
+if (!in_array(@$_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'])) {
+    die('You are not allowed to access this file.');
+}
+
+defined('YII_DEBUG') or define('YII_DEBUG', true);
+defined('YII_ENV') or define('YII_ENV', 'test');
+
+require __DIR__ . '/../vendor/autoload.php';
+require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
+
+$config = require __DIR__ . '/../config/test.php';
+
+(new yii\web\Application($config))->run();

+ 12 - 0
web/index.php

@@ -0,0 +1,12 @@
+<?php
+
+// comment out the following two lines when deployed to production
+defined('YII_DEBUG') or define('YII_DEBUG', true);
+defined('YII_ENV') or define('YII_ENV', 'dev');
+
+require __DIR__ . '/../vendor/autoload.php';
+require __DIR__ . '/../vendor/yiisoft/yii2/Yii.php';
+
+$config = require __DIR__ . '/../config/web.php';
+
+(new yii\web\Application($config))->run();

+ 2 - 0
web/info.php

@@ -0,0 +1,2 @@
+<?php
+phpinfo();

+ 2 - 0
web/robots.txt

@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:

+ 73 - 0
widgets/Alert.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace app\widgets;
+
+use Yii;
+
+/**
+ * Alert widget renders a message from session flash. All flash messages are displayed
+ * in the sequence they were assigned using setFlash. You can set message as following:
+ *
+ * ```php
+ * Yii::$app->session->setFlash('error', 'This is the message');
+ * Yii::$app->session->setFlash('success', 'This is the message');
+ * Yii::$app->session->setFlash('info', 'This is the message');
+ * ```
+ *
+ * Multiple messages could be set as follows:
+ *
+ * ```php
+ * Yii::$app->session->setFlash('error', ['Error 1', 'Error 2']);
+ * ```
+ *
+ * @author Kartik Visweswaran <kartikv2@gmail.com>
+ * @author Alexander Makarov <sam@rmcreative.ru>
+ */
+class Alert extends \yii\bootstrap4\Widget
+{
+    /**
+     * @var array the alert types configuration for the flash messages.
+     * This array is setup as $key => $value, where:
+     * - key: the name of the session flash variable
+     * - value: the bootstrap alert type (i.e. danger, success, info, warning)
+     */
+    public $alertTypes = [
+        'error'   => 'alert-danger',
+        'danger'  => 'alert-danger',
+        'success' => 'alert-success',
+        'info'    => 'alert-info',
+        'warning' => 'alert-warning'
+    ];
+    /**
+     * @var array the options for rendering the close button tag.
+     * Array will be passed to [[\yii\bootstrap\Alert::closeButton]].
+     */
+    public $closeButton = [];
+
+
+    /**
+     * {@inheritdoc}
+     */
+    public function run()
+    {
+        $session = Yii::$app->session;
+        $appendClass = isset($this->options['class']) ? ' ' . $this->options['class'] : '';
+
+        foreach (array_keys($this->alertTypes) as $type) {
+            $flash = $session->getFlash($type);
+
+            foreach ((array) $flash as $i => $message) {
+                echo \yii\bootstrap4\Alert::widget([
+                    'body' => $message,
+                    'closeButton' => $this->closeButton,
+                    'options' => array_merge($this->options, [
+                        'id' => $this->getId() . '-' . $type . '-' . $i,
+                        'class' => $this->alertTypes[$type] . $appendClass,
+                    ]),
+                ]);
+            }
+
+            $session->removeFlash($type);
+        }
+    }
+}

+ 21 - 0
yii

@@ -0,0 +1,21 @@
+#!/usr/bin/env php
+<?php
+/**
+ * Yii console bootstrap file.
+ *
+ * @link http://www.yiiframework.com/
+ * @copyright Copyright (c) 2008 Yii Software LLC
+ * @license http://www.yiiframework.com/license/
+ */
+
+defined('YII_DEBUG') or define('YII_DEBUG', true);
+defined('YII_ENV') or define('YII_ENV', 'dev');
+
+require __DIR__ . '/vendor/autoload.php';
+require __DIR__ . '/vendor/yiisoft/yii2/Yii.php';
+
+$config = require __DIR__ . '/config/console.php';
+
+$application = new yii\console\Application($config);
+$exitCode = $application->run();
+exit($exitCode);

+ 20 - 0
yii.bat

@@ -0,0 +1,20 @@
+@echo off
+
+rem -------------------------------------------------------------
+rem  Yii command line bootstrap script for Windows.
+rem
+rem  @author Qiang Xue <qiang.xue@gmail.com>
+rem  @link http://www.yiiframework.com/
+rem  @copyright Copyright (c) 2008 Yii Software LLC
+rem  @license http://www.yiiframework.com/license/
+rem -------------------------------------------------------------
+
+@setlocal
+
+set YII_PATH=%~dp0
+
+if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
+
+"%PHP_COMMAND%" "%YII_PATH%yii" %*
+
+@endlocal